mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-18 07:39:54 -06:00
Merge branch 'develop' into feature/backend-reachability-with-translations
This commit is contained in:
77
.github/workflows/distribution-deploy.yml
vendored
Normal file
77
.github/workflows/distribution-deploy.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
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: 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
|
||||
11
.github/workflows/poeditor-sync.yml
vendored
11
.github/workflows/poeditor-sync.yml
vendored
@@ -13,9 +13,6 @@ on:
|
||||
required: false
|
||||
default: "key_value_json"
|
||||
|
||||
# For automatic execution at a specific time (every day at midnight)
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -88,9 +85,9 @@ jobs:
|
||||
|
||||
- name: Copy translations to project
|
||||
run: |
|
||||
mkdir -p src/locales
|
||||
cp -r temp/* src/locales/
|
||||
echo "Translation files copied to src/locales/"
|
||||
mkdir -p client/src/locales
|
||||
cp -r temp/* client/src/locales/
|
||||
echo "Translation files copied to client/src/locales/"
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -113,6 +110,6 @@ jobs:
|
||||
delete-branch: true
|
||||
base: develop
|
||||
add-paths: |
|
||||
src/locales/*.json
|
||||
client/src/locales/*.json
|
||||
committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||
author: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||
|
||||
95
.github/workflows/production-deploy.yml
vendored
Normal file
95
.github/workflows/production-deploy.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: Demo deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["demo"]
|
||||
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-demo \
|
||||
-f ./docker/prod/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-demo
|
||||
|
||||
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-demo \
|
||||
-f ./docker/prod/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-demo
|
||||
|
||||
- name: Build Mongo Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:mongo-demo \
|
||||
-f ./docker/prod/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-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
|
||||
steps:
|
||||
- name: SSH into server and restart container using Docker Compose
|
||||
uses: appleboy/ssh-action@v1.2.2
|
||||
with:
|
||||
host: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
username: ${{ secrets.DEMO_SERVER_USER }}
|
||||
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd checkmate
|
||||
git pull
|
||||
cd docker/prod
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
4
.github/workflows/staging-deploy.yml
vendored
4
.github/workflows/staging-deploy.yml
vendored
@@ -87,7 +87,9 @@ jobs:
|
||||
username: ${{ secrets.STAGING_SERVER_USER }}
|
||||
key: ${{ secrets.STAGING_SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd checkmate/docker/staging
|
||||
cd checkmate
|
||||
git pull
|
||||
cd docker/staging
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
8
.github/workflows/upload-poeditor.yml
vendored
8
.github/workflows/upload-poeditor.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "src/locales/**"
|
||||
- "client/src/locales/**"
|
||||
|
||||
jobs:
|
||||
upload-translations:
|
||||
@@ -58,8 +58,8 @@ jobs:
|
||||
echo "Base SHA: $BASE_SHA"
|
||||
echo "Head SHA: $HEAD_SHA"
|
||||
|
||||
# Get list of changed files in src/locales directory
|
||||
CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA -- 'src/locales/*.json' || git ls-files 'src/locales/*.json')
|
||||
# Get list of changed files in client/src/locales directory
|
||||
CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA -- 'client/src/locales/*.json' || git ls-files 'client/src/locales/*.json')
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "No changes detected in locale files"
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
# Process each changed file
|
||||
for FILE in $CHANGED_FILES; do
|
||||
if [[ -f "$FILE" ]]; then
|
||||
# Extract language code from filename (e.g., src/locales/en.json -> en)
|
||||
# Extract language code from filename (e.g., client/src/locales/en.json -> en)
|
||||
FILENAME=$(basename "$FILE")
|
||||
|
||||
# Special case: map gb.json to en language code
|
||||
|
||||
@@ -48,24 +48,23 @@ Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) —
|
||||
|
||||
### Set up Checkmate locally?
|
||||
|
||||
Frontend:
|
||||
Frontend & backend:
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
Backend:
|
||||
Clone and follow instructions in [checkmate-backend](https://github.com/bluewave-labs/checkmate-backend).
|
||||
|
||||
By default, the frontend expects the backend on `http://localhost:3001`. Update configs if needed.
|
||||
|
||||
### Start contributing code?
|
||||
|
||||
1. Pick or open an issue (check `good-first-issue`s first)
|
||||
2. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well.
|
||||
3. Create a branch from `develop`.
|
||||
4. Write your code.
|
||||
5. Run and test locally.
|
||||
6. Open a PR to `develop`.
|
||||
2. (optional but highly suggested) Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||
3. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well.
|
||||
4. Create a branch from `develop`.
|
||||
5. Write your code.
|
||||
6. Run and test locally.
|
||||
7. Open a PR to `develop`.
|
||||
|
||||
Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
24
README.md
24
README.md
@@ -15,7 +15,7 @@
|
||||
|
||||

|
||||
|
||||
This repository contains the **frontend** 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.
|
||||
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.
|
||||
|
||||
@@ -23,11 +23,9 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
|
||||
We **love** what we are building here, and we continuously learn a few things about Reactjs, Nodejs, MongoDB, and Docker while building Checkmate.
|
||||
|
||||
For backend files, please check [Checkmate backend](https://github.com/bluewave-labs/checkmate-backend) repository.
|
||||
|
||||
## 📦 Demo
|
||||
|
||||
See [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||
|
||||
## 🔗 User's guide
|
||||
|
||||
@@ -71,11 +69,10 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
**Short term roadmap:**
|
||||
|
||||
- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593
|
||||
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
|
||||
- Translations (i18n) (**in progress**)
|
||||
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
|
||||
- JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573
|
||||
- JSON query monitoring (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1573
|
||||
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
|
||||
- More configuration options
|
||||
- DNS monitoring
|
||||
@@ -120,10 +117,11 @@ Here's how you can contribute:
|
||||
|
||||
0. Star this repo :)
|
||||
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
|
||||
2. Optionally, read [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||
3. Open an issue if you believe you've encountered a bug.
|
||||
4. Check for good-first-issue's if you are a newcomer.
|
||||
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
2. Check [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||
4. Open an issue if you believe you've encountered a bug.
|
||||
5. Check for good-first-issue's if you are a newcomer.
|
||||
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
@@ -137,8 +135,6 @@ Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their doc
|
||||
|
||||
Also check other developer and contributor-friendly projects of BlueWave:
|
||||
|
||||
- [LangRoute](https://github.com/bluewave-labs/langroute), an LLM proxy and gateway
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [Headcount](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
|
||||
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||
|
||||
144
client/README.md
144
client/README.md
@@ -1,144 +1,2 @@
|
||||
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://www.bestpractices.dev/projects/9901)
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
|
||||
|
||||

|
||||
|
||||
This repository contains the **frontend** 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 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.
|
||||
|
||||
For backend files, please check [Checkmate backend](https://github.com/bluewave-labs/checkmate-backend) repository.
|
||||
|
||||
## 📦 Demo
|
||||
|
||||
See [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||
|
||||
## 🔗 User's guide
|
||||
|
||||
Usage instructions can be found [here](https://docs.checkmate.so/). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) or [Elestio](https://elest.io/open-source/checkmate) for a one-click Docker deployment. 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.
|
||||
|
||||
## 🏁 Translations
|
||||
|
||||
If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to.
|
||||
|
||||
## 🚀 Performance
|
||||
|
||||
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
|
||||
|
||||

|
||||
|
||||
You can see the memory footprint of MongoDB and Redis on the same server (398Mb and 15Mb) for the same amount of servers:
|
||||
|
||||

|
||||
|
||||
## 💚 Questions & Ideas
|
||||
|
||||
If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
|
||||
|
||||
## 🧩 Features
|
||||
|
||||
- Completely open source, deployable on your servers
|
||||
- Website monitoring
|
||||
- Page speed monitoring
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
- Port monitoring
|
||||
- Incidents at a glance
|
||||
- E-mail notifications
|
||||
- Scheduled maintenance
|
||||
|
||||
**Short term roadmap:**
|
||||
|
||||
- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593
|
||||
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
|
||||
- Translations (i18n) (**in progress**)
|
||||
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
|
||||
- JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573
|
||||
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
|
||||
- More configuration options
|
||||
- DNS monitoring
|
||||
|
||||
## 🏗️ Screenshots
|
||||
|
||||
<p>
|
||||
<img width="2714" alt="server" src="https://github.com/user-attachments/assets/f7cb272a-69a6-48c5-93b0-249ecf20ecc6" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="2714" alt="uptime" src="https://github.com/user-attachments/assets/98ddc6c0-3384-47fd-96ce-7e53e6b688ac" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="2714" alt="page speed" src="https://github.com/user-attachments/assets/b5589f79-da30-4239-9846-1f8bb2637ff9" />
|
||||
</p>
|
||||
|
||||
## 🏗️ Tech stack
|
||||
|
||||
- [ReactJs](https://react.dev/)
|
||||
- [MUI (React framework)](https://mui.com/)
|
||||
- [Node.js](https://nodejs.org/en)
|
||||
- [MongoDB](https://mongodb.com)
|
||||
- [Recharts](https://recharts.org)
|
||||
- Lots of other open source components!
|
||||
|
||||
## A few links
|
||||
|
||||
- If you would like to support us, please consider giving it a ⭐ and click on "watch".
|
||||
- Have a question or suggestion for the roadmap/featureset? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.
|
||||
- Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases.
|
||||
- Watch a Checkmate [installation and usage video](https://www.youtube.com/watch?v=GfFOc0xHIwY)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
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), and [Aryaman](https://github.com/Br0wnHammer) 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 4.6K+ stars and attracted 60 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!
|
||||
|
||||
Here's how you can contribute:
|
||||
|
||||
0. Star this repo :)
|
||||
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
|
||||
2. Optionally, read [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||
3. Open an issue if you believe you've encountered a bug.
|
||||
4. Check for good-first-issue's if you are a newcomer.
|
||||
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
</a>
|
||||
|
||||
## 💰 Our sponsors
|
||||
|
||||
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
||||
|
||||
Also check other developer and contributor-friendly projects of BlueWave:
|
||||
|
||||
- [LangRoute](https://github.com/bluewave-labs/langroute), an LLM proxy and gateway
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [Headcount](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
|
||||
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
|
||||
This directory contains the client side (frontend) of Checkmate.
|
||||
|
||||
13698
client/package-lock.json
generated
13698
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,12 +22,6 @@
|
||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"@mui/material": "6.4.11",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"@solana/wallet-adapter-base": "0.9.25",
|
||||
"@solana/wallet-adapter-material-ui": "0.16.35",
|
||||
"@solana/wallet-adapter-react": "0.15.37",
|
||||
"@solana/wallet-adapter-react-ui": "0.9.37",
|
||||
"@solana/wallet-adapter-wallets": "0.19.34",
|
||||
"@solana/web3.js": "1.98.0",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
@@ -52,6 +46,14 @@
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"unusedDepencies": {
|
||||
"@solana/wallet-adapter-base": "0.9.25",
|
||||
"@solana/wallet-adapter-material-ui": "0.16.35",
|
||||
"@solana/wallet-adapter-react": "0.15.37",
|
||||
"@solana/wallet-adapter-react-ui": "0.9.37",
|
||||
"@solana/wallet-adapter-wallets": "0.19.34",
|
||||
"@solana/web3.js": "1.98.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
|
||||
6
client/public/bulk_import_monitors_sample.csv
Normal file
6
client/public/bulk_import_monitors_sample.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
|
||||
http,HTTP Monitor,https://www.google.com,60000,,,,
|
||||
http,API Monitor,http://reqres.in/api/users/2,120000,,Janet,data.first_name,equal
|
||||
ping,Ping monitor,127.0.0.1,60000,,,,
|
||||
docker,Docker monitor,bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01,60000,,,,
|
||||
port,Port monitor,127.0.0.1,60000,5000,,,
|
||||
|
1
client/public/bulk_import_monitors_template.csv
Normal file
1
client/public/bulk_import_monitors_template.csv
Normal file
@@ -0,0 +1 @@
|
||||
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
|
||||
|
@@ -1,43 +1,18 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import lightTheme from "./Utils/Theme/lightTheme";
|
||||
import darkTheme from "./Utils/Theme/darkTheme";
|
||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||
import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Routes } from "./Routes";
|
||||
import WalletProvider from "./Components/WalletProvider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { setLanguage } from "./Features/UI/uiSlice";
|
||||
|
||||
function App() {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
dispatch(getAppSettings({ authToken })).then((action) => {
|
||||
if (action.payload && action.payload.success) {
|
||||
const { language } = action.payload.data;
|
||||
const availableLanguages = Object.keys(i18n.options.resources || {});
|
||||
if (language && availableLanguages.includes(language)) {
|
||||
dispatch(setLanguage(language));
|
||||
i18n.changeLanguage(language);
|
||||
} else {
|
||||
dispatch(setLanguage(availableLanguages[0]));
|
||||
i18n.changeLanguage(availableLanguages[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [dispatch, authToken, i18n]);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
|
||||
@@ -118,6 +118,17 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, show
|
||||
>
|
||||
Let's create your first {title}
|
||||
</Button>
|
||||
{/* Bulk create of uptime monitors */}
|
||||
{title === "uptime monitor" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate("/uptime/bulk-import")}
|
||||
>
|
||||
{t("bulkImport.fallbackPage")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Warning box for PageSpeed monitor */}
|
||||
{(title === "pagespeed monitor" && showPageSpeedWarning) && (
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.image-field-wrapper h2.MuiTypography-root,
|
||||
.MuiStack-root:has(#modal-update-picture) button,
|
||||
.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.image-field-wrapper h2.MuiTypography-root {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.image-field-wrapper + p.MuiTypography-root {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.image-field-wrapper + p.MuiTypography-root,
|
||||
.image-field-wrapper p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, IconButton, Stack, TextField, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import "./index.css";
|
||||
import { checkImage } from "../../../Utils/fileUtils";
|
||||
|
||||
/**
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.id - The unique identifier for the input field.
|
||||
* @param {string} props.src - The URL of the image to display.
|
||||
* @param {function} props.onChange - The function to handle file input change.
|
||||
* @param {boolean} props.isRound - Whether the shape of the image to display is round.
|
||||
* @param {string} props.maxSize - Custom message for the max uploaded file size
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
|
||||
const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize }) => {
|
||||
const theme = useTheme();
|
||||
const error_border_style = error ? { borderColor: theme.palette.error.main } : {};
|
||||
|
||||
const roundShape = isRound ? { borderRadius: "50%" } : {};
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleDragEnter = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!checkImage(src) || loading ? (
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.primary.lowContrast,
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
...error_border_style,
|
||||
}}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDragLeave}
|
||||
>
|
||||
<TextField
|
||||
id={id}
|
||||
type="file"
|
||||
onChange={onChange}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
zIndex: 1,
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
className="custom-file-text"
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color="info"
|
||||
fontWeight={500}
|
||||
>
|
||||
Click to upload
|
||||
</Typography>{" "}
|
||||
or drag and drop
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
(maximum size: {maxSize ?? "3MB"})
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
Supported formats: JPG, PNG
|
||||
</Typography>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "250px",
|
||||
height: "250px",
|
||||
overflow: "hidden",
|
||||
backgroundImage: `url(${src})`,
|
||||
backgroundSize: "cover",
|
||||
...roundShape,
|
||||
}}
|
||||
></Box>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImageField.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
src: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isRound: PropTypes.bool,
|
||||
maxSize: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageField;
|
||||
243
client/src/Components/Inputs/ImageUpload/index.jsx
Normal file
243
client/src/Components/Inputs/ImageUpload/index.jsx
Normal file
@@ -0,0 +1,243 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import Image from "../../../Components/Image";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ProgressUpload from "../../ProgressBars";
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
import { useCallback, useState, useRef, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* ImageUpload component allows users to upload images with drag-and-drop functionality.
|
||||
* It supports file size and format validation.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props
|
||||
* @param {boolean} [props.previewIsRound=false] - Determines if the image preview should be round
|
||||
* @param {string} [props.src] - Source URL of the image to display
|
||||
* @param {function} props.onChange - Callback function to handle file change, takes a file as an argument
|
||||
* @param {number} [props.maxSize=3145728] - Maximum file size allowed in bytes (default is 3MB)
|
||||
* @param {Array<string>} [props.accept=['jpg', 'jpeg', 'png']] - Array of accepted file formats
|
||||
* @param {Object} [props.errors] - Object containing error messages
|
||||
* @returns {JSX.Element} The rendered component
|
||||
*/
|
||||
const ImageUpload = ({
|
||||
previewIsRound = false,
|
||||
src,
|
||||
onChange,
|
||||
maxSize = 3 * 1024 * 1024,
|
||||
accept = ["jpg", "jpeg", "png"],
|
||||
error,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [uploadComplete, setUploadComplete] = useState(false);
|
||||
const [completedFile, setCompletedFile] = useState(null);
|
||||
const [file, setFile] = useState(null);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
const intervalRef = useRef(null);
|
||||
const [localError, setLocalError] = useState(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
|
||||
|
||||
const handleImageChange = useCallback(
|
||||
(file) => {
|
||||
if (!file) return;
|
||||
|
||||
const isValidType = accept.some((type) =>
|
||||
file.type.includes(type)
|
||||
);
|
||||
const isValidSize = file.size <= maxSize;
|
||||
|
||||
if (!isValidType) {
|
||||
setLocalError(t('invalidFileFormat'));
|
||||
return;
|
||||
}
|
||||
if (!isValidSize) {
|
||||
setLocalError(t('invalidFileSize'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalError(null);
|
||||
|
||||
const previewFile = {
|
||||
src: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
file,
|
||||
};
|
||||
|
||||
setFile(previewFile);
|
||||
setProgress({ value: 0, isLoading: true });
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
const buffer = 12;
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
setUploadComplete(true);
|
||||
setCompletedFile(previewFile);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { value: prev.value + buffer, isLoading: true };
|
||||
});
|
||||
}, 120);
|
||||
},
|
||||
[maxSize, accept]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadComplete && completedFile) {
|
||||
onChange?.(completedFile);
|
||||
setUploadComplete(false);
|
||||
setCompletedFile(null);
|
||||
}
|
||||
}, [uploadComplete, completedFile, onChange]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{src ? (
|
||||
<Stack direction="row" justifyContent="center">
|
||||
<Image
|
||||
alt="Uploaded preview"
|
||||
src={src}
|
||||
width="250px"
|
||||
height="250px"
|
||||
sx={{ ...roundStyle }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
onDragEnter={() => setIsDragging(true)}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={() => setIsDragging(false)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.primary.lowContrast,
|
||||
backgroundColor: isDragging
|
||||
? "hsl(215, 87%, 51%, 0.05)"
|
||||
: "transparent",
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="file"
|
||||
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
zIndex: 1,
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color="info"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t('ClickUpload')}
|
||||
</Typography>{" "}
|
||||
or {t('DragandDrop')}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>({t('MaxSize')}: {Math.round(maxSize / 1024 / 1024)}MB)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={file?.name || "Upload failed"}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
onClick={() => {
|
||||
clearInterval(intervalRef.current);
|
||||
setFile(null);
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setLocalError(null);
|
||||
onChange(undefined);
|
||||
}}
|
||||
error={localError || error}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{t('SupportedFormats')}: {accept.join(", ").toUpperCase()}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
previewIsRound: PropTypes.bool,
|
||||
src: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
maxSize: PropTypes.number,
|
||||
accept: PropTypes.array,
|
||||
error: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
@@ -1,30 +1,49 @@
|
||||
import { Stack, Button } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const CreateMonitorHeader = ({
|
||||
isAdmin,
|
||||
label = "Create new",
|
||||
shouldRender = true,
|
||||
isLoading = true,
|
||||
path,
|
||||
bulkPath,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!isAdmin) return null;
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{bulkPath && (
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
navigate(`${bulkPath}`);
|
||||
}}
|
||||
>
|
||||
{t("bulkImport.title")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -33,7 +52,8 @@ export default CreateMonitorHeader;
|
||||
|
||||
CreateMonitorHeader.propTypes = {
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
shouldRender: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
path: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
bulkPath: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -25,10 +25,12 @@ const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography variant="h2">
|
||||
<Typography variant="h2"
|
||||
style={{fontFamily : "monospace" , fontWeight : 'bolder'}}
|
||||
>
|
||||
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Dot />
|
||||
|
||||
@@ -55,10 +55,9 @@ const MonitorTimeFrameHeader = ({
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
Showing statistics for past{" "}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "./TabPanel";
|
||||
@@ -19,335 +20,400 @@ import useNotifications from "../Hooks/useNotification";
|
||||
|
||||
// Define constants for notification types to avoid magic values
|
||||
const NOTIFICATION_TYPES = {
|
||||
SLACK: 'slack',
|
||||
DISCORD: 'discord',
|
||||
TELEGRAM: 'telegram',
|
||||
WEBHOOK: 'webhook'
|
||||
SLACK: "slack",
|
||||
DISCORD: "discord",
|
||||
TELEGRAM: "telegram",
|
||||
WEBHOOK: "webhook",
|
||||
};
|
||||
|
||||
// Define constants for field IDs
|
||||
const FIELD_IDS = {
|
||||
WEBHOOK: 'webhook',
|
||||
TOKEN: 'token',
|
||||
CHAT_ID: 'chatId',
|
||||
URL: 'url'
|
||||
WEBHOOK: "webhook",
|
||||
TOKEN: "token",
|
||||
CHAT_ID: "chatId",
|
||||
URL: "url",
|
||||
};
|
||||
|
||||
const NotificationIntegrationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
monitor,
|
||||
setMonitor,
|
||||
// Optional prop to configure available notification types
|
||||
notificationTypes = null
|
||||
const NotificationIntegrationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
monitor,
|
||||
setMonitor,
|
||||
// Optional prop to configure available notification types
|
||||
notificationTypes = null,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
const [loading, _, sendTestNotification] = useNotifications();
|
||||
|
||||
// Helper to get the field state key with error handling
|
||||
const getFieldKey = (typeId, fieldId) => {
|
||||
if (typeof typeId !== 'string' || typeId === '') {
|
||||
throw new Error(t('errorInvalidTypeId'));
|
||||
}
|
||||
|
||||
if (typeof fieldId !== 'string' || fieldId === '') {
|
||||
throw new Error(t('errorInvalidFieldId'));
|
||||
}
|
||||
|
||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
||||
};
|
||||
|
||||
// Define notification types
|
||||
const DEFAULT_NOTIFICATION_TYPES = [
|
||||
{
|
||||
id: NOTIFICATION_TYPES.SLACK,
|
||||
label: t('notifications.slack.label'),
|
||||
description: t('notifications.slack.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t('notifications.slack.webhookLabel'),
|
||||
placeholder: t('notifications.slack.webhookPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.DISCORD,
|
||||
label: t('notifications.discord.label'),
|
||||
description: t('notifications.discord.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t('notifications.discord.webhookLabel'),
|
||||
placeholder: t('notifications.discord.webhookPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.TELEGRAM,
|
||||
label: t('notifications.telegram.label'),
|
||||
description: t('notifications.telegram.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.TOKEN,
|
||||
label: t('notifications.telegram.tokenLabel'),
|
||||
placeholder: t('notifications.telegram.tokenPlaceholder'),
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: FIELD_IDS.CHAT_ID,
|
||||
label: t('notifications.telegram.chatIdLabel'),
|
||||
placeholder: t('notifications.telegram.chatIdPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.WEBHOOK,
|
||||
label: t('notifications.webhook.label'),
|
||||
description: t('notifications.webhook.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.URL,
|
||||
label: t('notifications.webhook.urlLabel'),
|
||||
placeholder: t('notifications.webhook.urlPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
// Use provided notification types or default to our translated ones
|
||||
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
|
||||
|
||||
// Memoized function to initialize integrations state
|
||||
const initialIntegrationsState = useMemo(() => {
|
||||
const state = {};
|
||||
|
||||
activeNotificationTypes.forEach(type => {
|
||||
// Add enabled flag for each notification type
|
||||
state[type.id] = monitor?.notifications?.some(n => n.type === type.id) || false;
|
||||
|
||||
// Add state for each field in the notification type
|
||||
type.fields.forEach(field => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
state[fieldKey] = monitor?.notifications?.find(n => n.type === type.id)?.[field.id] || "";
|
||||
});
|
||||
});
|
||||
|
||||
return state;
|
||||
}, [monitor, activeNotificationTypes]); // Only recompute when these dependencies change
|
||||
|
||||
const [integrations, setIntegrations] = useState(initialIntegrationsState);
|
||||
const [loading, _, sendTestNotification] = useNotifications();
|
||||
|
||||
const handleChangeTab = (event, newValue) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
// Helper to get the field state key with error handling
|
||||
const getFieldKey = useCallback(
|
||||
(typeId, fieldId) => {
|
||||
if (typeof typeId !== "string" || typeId === "") {
|
||||
throw new Error(t("errorInvalidTypeId"));
|
||||
}
|
||||
|
||||
const handleIntegrationChange = (type, checked) => {
|
||||
setIntegrations(prev => ({
|
||||
...prev,
|
||||
[type]: checked
|
||||
}));
|
||||
};
|
||||
if (typeof fieldId !== "string" || fieldId === "") {
|
||||
throw new Error(t("errorInvalidFieldId"));
|
||||
}
|
||||
|
||||
const handleInputChange = (type, value) => {
|
||||
setIntegrations(prev => ({
|
||||
...prev,
|
||||
[type]: value
|
||||
}));
|
||||
};
|
||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleTestNotification = async (type) => {
|
||||
// Get the notification type details
|
||||
const notificationType = activeNotificationTypes.find(t => t.id === type);
|
||||
|
||||
if (typeof notificationType === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare config object based on notification type
|
||||
const config = {};
|
||||
|
||||
// Add each field value to the config object
|
||||
notificationType.fields.forEach(field => {
|
||||
const fieldKey = getFieldKey(type, field.id);
|
||||
config[field.id] = integrations[fieldKey];
|
||||
});
|
||||
|
||||
await sendTestNotification(type, config);
|
||||
};
|
||||
|
||||
// In NotificationIntegrationModal.jsx, update the handleSave function:
|
||||
// Define notification types
|
||||
const DEFAULT_NOTIFICATION_TYPES = [
|
||||
{
|
||||
id: NOTIFICATION_TYPES.SLACK,
|
||||
label: t("notifications.slack.label"),
|
||||
description: t("notifications.slack.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t("notifications.slack.webhookLabel"),
|
||||
placeholder: t("notifications.slack.webhookPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.DISCORD,
|
||||
label: t("notifications.discord.label"),
|
||||
description: t("notifications.discord.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t("notifications.discord.webhookLabel"),
|
||||
placeholder: t("notifications.discord.webhookPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.TELEGRAM,
|
||||
label: t("notifications.telegram.label"),
|
||||
description: t("notifications.telegram.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.TOKEN,
|
||||
label: t("notifications.telegram.tokenLabel"),
|
||||
placeholder: t("notifications.telegram.tokenPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: FIELD_IDS.CHAT_ID,
|
||||
label: t("notifications.telegram.chatIdLabel"),
|
||||
placeholder: t("notifications.telegram.chatIdPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.WEBHOOK,
|
||||
label: t("notifications.webhook.label"),
|
||||
description: t("notifications.webhook.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.URL,
|
||||
label: t("notifications.webhook.urlLabel"),
|
||||
placeholder: t("notifications.webhook.urlPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleSave = () => {
|
||||
// Get existing notifications
|
||||
const notifications = [...(monitor?.notifications || [])];
|
||||
|
||||
// Get all notification types IDs
|
||||
const existingTypes = activeNotificationTypes.map(type => type.id);
|
||||
|
||||
// Filter out notifications that are configurable in this modal
|
||||
const filteredNotifications = notifications.filter(
|
||||
notification => {
|
||||
|
||||
if (notification.platform) {
|
||||
return !existingTypes.includes(notification.platform);
|
||||
}
|
||||
|
||||
return !existingTypes.includes(notification.type);
|
||||
}
|
||||
);
|
||||
// Use provided notification types or default to our translated ones
|
||||
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
|
||||
|
||||
// Add each enabled notification with its configured fields
|
||||
activeNotificationTypes.forEach(type => {
|
||||
if (integrations[type.id]) {
|
||||
// Memoized function to initialize integrations state
|
||||
const initialIntegrationsState = useMemo(() => {
|
||||
const state = {};
|
||||
|
||||
let notificationObject = {
|
||||
type: "webhook",
|
||||
platform: type.id, // Set platform to identify the specific service
|
||||
config: {}
|
||||
};
|
||||
|
||||
// Configure based on notification type
|
||||
switch(type.id) {
|
||||
case "slack":
|
||||
case "discord":
|
||||
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'webhook')];
|
||||
break;
|
||||
case "telegram":
|
||||
notificationObject.config.botToken = integrations[getFieldKey(type.id, 'token')];
|
||||
notificationObject.config.chatId = integrations[getFieldKey(type.id, 'chatId')];
|
||||
break;
|
||||
case "webhook":
|
||||
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'url')];
|
||||
break;
|
||||
}
|
||||
|
||||
filteredNotifications.push(notificationObject);
|
||||
}
|
||||
});
|
||||
activeNotificationTypes.forEach((type) => {
|
||||
// Add enabled flag for each notification type
|
||||
state[type.id] = false;
|
||||
|
||||
// Update monitor with new notifications
|
||||
setMonitor(prev => ({
|
||||
...prev,
|
||||
notifications: filteredNotifications
|
||||
}));
|
||||
|
||||
onClose();
|
||||
};
|
||||
// Add state for each field in the notification type
|
||||
type.fields.forEach((field) => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
state[fieldKey] = "";
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
width: `calc(80% - ${theme.spacing(40)})`,
|
||||
maxWidth: `${theme.breakpoints.values.md - 70}px`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
height: `calc(26vh - ${theme.spacing(20)})`
|
||||
}}>
|
||||
{/* Left sidebar with tabs */}
|
||||
<Box sx={{
|
||||
borderRight: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
width: '30%',
|
||||
maxWidth: theme.spacing(120),
|
||||
pr: theme.spacing(10)
|
||||
}}>
|
||||
<Typography variant="subtitle1" sx={{
|
||||
my: theme.spacing(1),
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.typography.fontSize * 0.9,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
pl: theme.spacing(4)
|
||||
}}>
|
||||
{t('notifications.addOrEditNotifications')}
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
variant="scrollable"
|
||||
value={tabValue}
|
||||
onChange={handleChangeTab}
|
||||
aria-label="Notification tabs"
|
||||
>
|
||||
{activeNotificationTypes.map((type) => (
|
||||
<Tab
|
||||
key={type.id}
|
||||
label={type.label}
|
||||
orientation="vertical"
|
||||
disableRipple
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
return state;
|
||||
}, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change
|
||||
|
||||
{/* Right side content */}
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
pl: theme.spacing(7.5),
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{activeNotificationTypes.map((type, index) => (
|
||||
<TabPanel key={type.id} value={tabValue} index={index}>
|
||||
<TabComponent
|
||||
type={type}
|
||||
integrations={integrations}
|
||||
handleIntegrationChange={handleIntegrationChange}
|
||||
handleInputChange={handleInputChange}
|
||||
handleTestNotification={handleTestNotification}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{
|
||||
p: theme.spacing(4),
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
mb: theme.spacing(5),
|
||||
mr: theme.spacing(5)
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
sx={{
|
||||
width: 'auto',
|
||||
minWidth: theme.spacing(60),
|
||||
px: theme.spacing(8)
|
||||
}}
|
||||
>
|
||||
{t('commonSave')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
const [integrations, setIntegrations] = useState(initialIntegrationsState);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const extractNotificationValues = () => {
|
||||
const values = {};
|
||||
|
||||
if (!monitor?.notifications || !Array.isArray(monitor.notifications)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
monitor.notifications.forEach((notification) => {
|
||||
// Handle notification based on its structure
|
||||
if (notification.type === "webhook" && notification.platform) {
|
||||
if (typeof notification.config === "undefined") return;
|
||||
const platform = notification.platform;
|
||||
values[platform] = true; // Set platform as enabled
|
||||
|
||||
// Extract configuration based on platform
|
||||
switch (platform) {
|
||||
case NOTIFICATION_TYPES.SLACK:
|
||||
case NOTIFICATION_TYPES.DISCORD:
|
||||
if (notification.config.webhookUrl) {
|
||||
values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] =
|
||||
notification.config.webhookUrl;
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPES.TELEGRAM:
|
||||
if (notification.config.botToken) {
|
||||
values[getFieldKey(platform, FIELD_IDS.TOKEN)] =
|
||||
notification.config.botToken;
|
||||
}
|
||||
if (notification.config.chatId) {
|
||||
values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] =
|
||||
notification.config.chatId;
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPES.WEBHOOK:
|
||||
if (notification.config.webhookUrl) {
|
||||
values[getFieldKey(platform, FIELD_IDS.URL)] =
|
||||
notification.config.webhookUrl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
const extractedValues = extractNotificationValues();
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
...extractedValues,
|
||||
}));
|
||||
}
|
||||
}, [open, monitor, getFieldKey]);
|
||||
|
||||
const handleChangeTab = (event, newValue) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleIntegrationChange = (type, checked) => {
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
[type]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleInputChange = (type, value) => {
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
[type]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTestNotification = async (type) => {
|
||||
// Get the notification type details
|
||||
const notificationType = activeNotificationTypes.find((t) => t.id === type);
|
||||
|
||||
if (typeof notificationType === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare config object based on notification type
|
||||
const config = {};
|
||||
|
||||
// Add each field value to the config object
|
||||
notificationType.fields.forEach((field) => {
|
||||
const fieldKey = getFieldKey(type, field.id);
|
||||
config[field.id] = integrations[fieldKey];
|
||||
});
|
||||
|
||||
await sendTestNotification(type, config);
|
||||
};
|
||||
|
||||
// In NotificationIntegrationModal.jsx, update the handleSave function:
|
||||
|
||||
const handleSave = () => {
|
||||
// Get existing notifications
|
||||
const notifications = [...(monitor?.notifications || [])];
|
||||
|
||||
// Get all notification types IDs
|
||||
const existingTypes = activeNotificationTypes.map((type) => type.id);
|
||||
|
||||
// Filter out notifications that are configurable in this modal
|
||||
const filteredNotifications = notifications.filter((notification) => {
|
||||
if (notification.platform) {
|
||||
return !existingTypes.includes(notification.platform);
|
||||
}
|
||||
|
||||
return !existingTypes.includes(notification.type);
|
||||
});
|
||||
|
||||
// Add each enabled notification with its configured fields
|
||||
activeNotificationTypes.forEach((type) => {
|
||||
if (integrations[type.id]) {
|
||||
let notificationObject = {
|
||||
type: "webhook",
|
||||
platform: type.id, // Set platform to identify the specific service
|
||||
config: {},
|
||||
};
|
||||
|
||||
// Configure based on notification type
|
||||
switch (type.id) {
|
||||
case "slack":
|
||||
case "discord":
|
||||
notificationObject.config.webhookUrl =
|
||||
integrations[getFieldKey(type.id, "webhook")];
|
||||
break;
|
||||
case "telegram":
|
||||
notificationObject.config.botToken =
|
||||
integrations[getFieldKey(type.id, "token")];
|
||||
notificationObject.config.chatId =
|
||||
integrations[getFieldKey(type.id, "chatId")];
|
||||
break;
|
||||
case "webhook":
|
||||
notificationObject.config.webhookUrl =
|
||||
integrations[getFieldKey(type.id, "url")];
|
||||
break;
|
||||
}
|
||||
|
||||
filteredNotifications.push(notificationObject);
|
||||
}
|
||||
});
|
||||
|
||||
// Update monitor with new notifications
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
notifications: filteredNotifications,
|
||||
}));
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
sx={{
|
||||
"& .MuiDialog-paper": {
|
||||
width: `calc(80% - ${theme.spacing(40)})`,
|
||||
maxWidth: `${theme.breakpoints.values.md - 70}px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
height: `calc(30vh - ${theme.spacing(20)})`,
|
||||
}}
|
||||
>
|
||||
{/* Left sidebar with tabs */}
|
||||
<Box
|
||||
sx={{
|
||||
borderRight: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
width: "30%",
|
||||
maxWidth: theme.spacing(120),
|
||||
pr: theme.spacing(10),
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2">
|
||||
{t("notifications.addOrEditNotifications")}
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
variant="scrollable"
|
||||
value={tabValue}
|
||||
onChange={handleChangeTab}
|
||||
aria-label="Notification tabs"
|
||||
>
|
||||
{activeNotificationTypes.map((type) => (
|
||||
<Tab
|
||||
key={type.id}
|
||||
label={type.label}
|
||||
orientation="vertical"
|
||||
disableRipple
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Right side content */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
pl: theme.spacing(7.5),
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{activeNotificationTypes.map((type, index) => (
|
||||
<TabPanel
|
||||
key={type.id}
|
||||
value={tabValue}
|
||||
index={index}
|
||||
>
|
||||
<TabComponent
|
||||
type={type}
|
||||
integrations={integrations}
|
||||
handleIntegrationChange={handleIntegrationChange}
|
||||
handleInputChange={handleInputChange}
|
||||
handleTestNotification={handleTestNotification}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: theme.spacing(4),
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
mb: theme.spacing(5),
|
||||
mr: theme.spacing(5),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
sx={{
|
||||
width: "auto",
|
||||
minWidth: theme.spacing(60),
|
||||
px: theme.spacing(8),
|
||||
}}
|
||||
>
|
||||
{t("commonSave")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationIntegrationModal.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
setMonitor: PropTypes.func.isRequired,
|
||||
notificationTypes: PropTypes.array
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
setMonitor: PropTypes.func.isRequired,
|
||||
notificationTypes: PropTypes.array,
|
||||
};
|
||||
|
||||
export default NotificationIntegrationModal;
|
||||
export default NotificationIntegrationModal;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { getTouchedFieldErrors } from "../../../Validation/error";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const defaultPasswordsState = {
|
||||
password: "",
|
||||
@@ -26,6 +27,7 @@ const defaultPasswordsState = {
|
||||
const PasswordPanel = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const SPACING_GAP = theme.spacing(12);
|
||||
|
||||
@@ -203,7 +205,7 @@ const PasswordPanel = () => {
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder="Reenter your new password"
|
||||
placeholder={t("confirmPassword")}
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import ImageField from "../../Inputs/Image";
|
||||
import { credentials, imageValidation } from "../../../Validation/validation";
|
||||
import ImageUpload from "../../Inputs/ImageUpload";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
import ProgressUpload from "../../ProgressBars";
|
||||
import { formatBytes } from "../../../Utils/fileUtils";
|
||||
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import Dialog from "../../Dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* ProfilePanel component displays a form for editing user profile information
|
||||
@@ -28,7 +26,7 @@ import Dialog from "../../Dialog";
|
||||
const ProfilePanel = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const SPACING_GAP = theme.spacing(12);
|
||||
|
||||
//redux state
|
||||
@@ -49,8 +47,6 @@ const ProfilePanel = () => {
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [file, setFile] = useState();
|
||||
const intervalRef = useRef(null);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
|
||||
// Handles input field changes and performs validation
|
||||
const handleChange = (event) => {
|
||||
@@ -65,33 +61,6 @@ const ProfilePanel = () => {
|
||||
validateField({ [name]: value }, credentials, name);
|
||||
};
|
||||
|
||||
// Handles image file
|
||||
const handlePicture = (event) => {
|
||||
const pic = event.target.files[0];
|
||||
let error = validateField({ type: pic.type, size: pic.size }, imageValidation);
|
||||
if (error) return;
|
||||
|
||||
setProgress((prev) => ({ ...prev, isLoading: true }));
|
||||
setFile({
|
||||
src: URL.createObjectURL(pic),
|
||||
name: pic.name,
|
||||
size: formatBytes(pic.size),
|
||||
delete: false,
|
||||
});
|
||||
|
||||
//TODO - potentitally remove, will revisit in the future
|
||||
intervalRef.current = setInterval(() => {
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
}, 120);
|
||||
};
|
||||
|
||||
// Validates input against provided schema and updates error state
|
||||
const validateField = (toValidate, schema, name = "picture") => {
|
||||
const { error } = schema.validate(toValidate, { abortEarly: false });
|
||||
@@ -116,52 +85,55 @@ const ProfilePanel = () => {
|
||||
// Resets picture-related states and clears interval
|
||||
const removePicture = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile({ delete: true });
|
||||
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
};
|
||||
setFile(undefined);
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
file: undefined,
|
||||
deleteProfileImage: true,
|
||||
}));
|
||||
};
|
||||
|
||||
// Opens the picture update modal
|
||||
const openPictureModal = () => {
|
||||
setIsOpen("picture");
|
||||
setFile({ delete: localData.deleteProfileImage });
|
||||
setFile(undefined);
|
||||
};
|
||||
|
||||
// Closes the picture update modal and resets related states
|
||||
const closePictureModal = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile(); //reset file
|
||||
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setIsOpen("");
|
||||
};
|
||||
if (errors["picture"]) clearError("picture");
|
||||
setFile(undefined);
|
||||
setIsOpen("");
|
||||
};
|
||||
|
||||
// Updates profile image displayed on UI
|
||||
const handleUpdatePicture = () => {
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
file: file.src,
|
||||
file: file?.src,
|
||||
deleteProfileImage: false,
|
||||
}));
|
||||
setIsOpen("");
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
if (errors["unchanged"]) clearError("unchanged");
|
||||
};
|
||||
|
||||
|
||||
// Handles form submission to update user profile
|
||||
const handleSaveProfile = async (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
localData.firstName === user.firstName &&
|
||||
localData.lastName === user.lastName &&
|
||||
localData.deleteProfileImage === undefined &&
|
||||
localData.file === undefined
|
||||
) {
|
||||
createToast({
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
return;
|
||||
const nameChanged =
|
||||
localData.firstName !== user.firstName ||
|
||||
localData.lastName !== user.lastName;
|
||||
|
||||
const avatarChanged =
|
||||
localData.deleteProfileImage === true ||
|
||||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
|
||||
|
||||
if (!nameChanged && !avatarChanged) {
|
||||
createToast({
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
return;
|
||||
}
|
||||
|
||||
const action = await dispatch(update({ localData }));
|
||||
@@ -182,6 +154,7 @@ const ProfilePanel = () => {
|
||||
...prev,
|
||||
deleteProfileImage: true,
|
||||
}));
|
||||
setFile(undefined);
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
};
|
||||
|
||||
@@ -232,7 +205,7 @@ const ProfilePanel = () => {
|
||||
>
|
||||
{/* This 0.9 is a bit magic numbering, refactor */}
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">First name</Typography>
|
||||
<Typography component="h1">{t('FirstName')}</Typography>
|
||||
</Box>
|
||||
<TextInput
|
||||
id="edit-first-name"
|
||||
@@ -250,7 +223,7 @@ const ProfilePanel = () => {
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Last name</Typography>
|
||||
<Typography component="h1">{t('LastName')}</Typography>
|
||||
</Box>
|
||||
<TextInput
|
||||
id="edit-last-name"
|
||||
@@ -268,12 +241,12 @@ const ProfilePanel = () => {
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Email</Typography>
|
||||
<Typography component="h1">{t('email')}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
This is your current email address — it cannot be changed.
|
||||
{t('EmailDescriptionText')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<TextInput
|
||||
@@ -291,12 +264,12 @@ const ProfilePanel = () => {
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Your photo</Typography>
|
||||
<Typography component="h1">{t('YourPhoto')}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
This photo will be displayed in your profile page.
|
||||
{t('PhotoDescriptionText')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -320,14 +293,14 @@ const ProfilePanel = () => {
|
||||
color="error"
|
||||
onClick={handleDeletePicture}
|
||||
>
|
||||
Delete
|
||||
{t('delete')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained" // CAIO_REVIEW
|
||||
color="accent"
|
||||
onClick={openPictureModal}
|
||||
>
|
||||
Update
|
||||
{t('update')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -352,7 +325,7 @@ const ProfilePanel = () => {
|
||||
disabled={Object.keys(errors).length !== 0 && !errors?.picture && true}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
{t('save')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -371,13 +344,12 @@ const ProfilePanel = () => {
|
||||
spellCheck="false"
|
||||
>
|
||||
<Box mb={theme.spacing(6)}>
|
||||
<Typography component="h1">Delete account</Typography>
|
||||
<Typography component="h1">{t('DeleteAccountTitle')}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
Note that deleting your account will remove all data from the server. This
|
||||
is permanent and non-recoverable.
|
||||
{t('DeleteDescriptionText')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -385,19 +357,17 @@ const ProfilePanel = () => {
|
||||
color="error"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
>
|
||||
Delete account
|
||||
{t('DeleteAccountButton')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Dialog
|
||||
open={isModalOpen("delete")}
|
||||
theme={theme}
|
||||
title={"Really delete this account?"}
|
||||
description={
|
||||
"If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action."
|
||||
}
|
||||
title={t('DeleteWarningTitle')}
|
||||
description={t('DeleteAccountWarning')}
|
||||
onCancel={() => setIsOpen("")}
|
||||
confirmationButtonLabel={"Delete account"}
|
||||
confirmationButtonLabel={t('DeleteAccountButton')}
|
||||
onConfirm={handleDeleteAccount}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
@@ -408,34 +378,27 @@ const ProfilePanel = () => {
|
||||
onClose={closePictureModal}
|
||||
theme={theme}
|
||||
>
|
||||
<ImageField
|
||||
id="update-profile-picture"
|
||||
<ImageUpload
|
||||
src={
|
||||
file?.delete
|
||||
? ""
|
||||
: file?.src
|
||||
? file.src
|
||||
file?.src
|
||||
? file.src
|
||||
: localData?.deleteProfileImage
|
||||
? ""
|
||||
: localData?.file
|
||||
? localData.file
|
||||
: user?.avatarImage
|
||||
? `data:image/png;base64,${user.avatarImage}`
|
||||
: ""
|
||||
}
|
||||
loading={progress.isLoading && progress.value !== 100}
|
||||
onChange={handlePicture}
|
||||
}
|
||||
onChange={(newFile) => {
|
||||
if (newFile) {
|
||||
setFile(newFile);
|
||||
clearError("unchanged");
|
||||
}
|
||||
}}
|
||||
previewIsRound
|
||||
maxSize={3 * 1024 * 1024}
|
||||
/>
|
||||
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={file?.name}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
onClick={removePicture}
|
||||
error={errors["picture"]}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
mt={theme.spacing(10)}
|
||||
@@ -447,20 +410,15 @@ const ProfilePanel = () => {
|
||||
color="info"
|
||||
onClick={removePicture}
|
||||
>
|
||||
Remove
|
||||
{t('remove')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleUpdatePicture}
|
||||
disabled={
|
||||
(Object.keys(errors).length !== 0 && errors?.picture) ||
|
||||
progress.value !== 100
|
||||
? true
|
||||
: false
|
||||
}
|
||||
disabled={!!errors.picture || !file?.src}
|
||||
>
|
||||
Update
|
||||
{t('update')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</GenericDialog>
|
||||
|
||||
@@ -25,7 +25,7 @@ const TeamPanel = () => {
|
||||
|
||||
const [toInvite, setToInvite] = useState({
|
||||
email: "",
|
||||
role: ["0"],
|
||||
role: ["user"],
|
||||
});
|
||||
const [data, setData] = useState([]);
|
||||
const [members, setMembers] = useState([]);
|
||||
@@ -107,7 +107,7 @@ const TeamPanel = () => {
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
const newEmail = value?.toLowerCase() || value
|
||||
const newEmail = value?.toLowerCase() || value;
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
email: newEmail,
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import { useMemo } from "react";
|
||||
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
|
||||
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
|
||||
import {
|
||||
UnsafeBurnerWalletAdapter,
|
||||
PhantomWalletAdapter,
|
||||
} from "@solana/wallet-adapter-wallets";
|
||||
// import { useMemo } from "react";
|
||||
// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
|
||||
// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
|
||||
// import {
|
||||
// UnsafeBurnerWalletAdapter,
|
||||
// PhantomWalletAdapter,
|
||||
// } from "@solana/wallet-adapter-wallets";
|
||||
|
||||
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
|
||||
import { clusterApiUrl } from "@solana/web3.js";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
|
||||
// import { clusterApiUrl } from "@solana/web3.js";
|
||||
// import PropTypes from "prop-types";
|
||||
// import "./index.css";
|
||||
|
||||
// Default styles that can be overridden by your app
|
||||
import "@solana/wallet-adapter-react-ui/styles.css";
|
||||
// // Default styles that can be overridden by your app
|
||||
// import "@solana/wallet-adapter-react-ui/styles.css";
|
||||
|
||||
export const Wallet = ({ children }) => {
|
||||
// The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
|
||||
const network = WalletAdapterNetwork.Mainnet;
|
||||
// export const Wallet = ({ children }) => {
|
||||
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
|
||||
// const network = WalletAdapterNetwork.Mainnet;
|
||||
|
||||
// You can also provide a custom RPC endpoint.
|
||||
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
|
||||
// // You can also provide a custom RPC endpoint.
|
||||
// const endpoint = useMemo(() => clusterApiUrl(network), [network]);
|
||||
|
||||
const wallets = useMemo(
|
||||
() => [new PhantomWalletAdapter()],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[network]
|
||||
);
|
||||
// const wallets = useMemo(
|
||||
// () => [new PhantomWalletAdapter()],
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// [network]
|
||||
// );
|
||||
|
||||
return (
|
||||
<ConnectionProvider endpoint={endpoint}>
|
||||
<WalletProvider
|
||||
wallets={wallets}
|
||||
autoConnect
|
||||
>
|
||||
<WalletModalProvider>{children}</WalletModalProvider>
|
||||
</WalletProvider>
|
||||
</ConnectionProvider>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <ConnectionProvider endpoint={endpoint}>
|
||||
// <WalletProvider
|
||||
// wallets={wallets}
|
||||
// autoConnect
|
||||
// >
|
||||
// <WalletModalProvider>{children}</WalletModalProvider>
|
||||
// </WalletProvider>
|
||||
// </ConnectionProvider>
|
||||
// );
|
||||
// };
|
||||
|
||||
Wallet.propTypes = {
|
||||
children: PropTypes.node,
|
||||
// Wallet.propTypes = {
|
||||
// children: PropTypes.node,
|
||||
// };
|
||||
|
||||
const Wallet = ({ children }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
export default Wallet;
|
||||
|
||||
@@ -101,7 +101,7 @@ export const updatePageSpeed = createAsyncThunk(
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
// notifications: monitor.notifications,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
|
||||
@@ -5,7 +5,7 @@ const initialState = {
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
language: "",
|
||||
language: "gb",
|
||||
};
|
||||
|
||||
export const getAppSettings = createAsyncThunk(
|
||||
@@ -30,26 +30,9 @@ export const getAppSettings = createAsyncThunk(
|
||||
export const updateAppSettings = createAsyncThunk(
|
||||
"settings/updateSettings",
|
||||
async ({ settings }, thunkApi) => {
|
||||
// The reason for commenting is that, previously, we had the flexibility to set the API base.
|
||||
// However, now this could lead to an issue where it gets set to undefined.
|
||||
// networkService.setBaseUrl(settings.apiBaseUrl);
|
||||
try {
|
||||
const parsedSettings = {
|
||||
apiBaseUrl: settings.apiBaseUrl,
|
||||
logLevel: settings.logLevel,
|
||||
language: settings.language,
|
||||
clientHost: settings.clientHost,
|
||||
jwtSecret: settings.jwtSecret,
|
||||
dbType: settings.dbType,
|
||||
dbConnectionString: settings.dbConnectionString,
|
||||
redisHost: settings.redisHost,
|
||||
redisPort: settings.redisPort,
|
||||
jwtTTL: settings.jwtTTL,
|
||||
pagespeedApiKey: settings.pagespeedApiKey,
|
||||
systemEmailHost: settings.systemEmailHost,
|
||||
systemEmailPort: settings.systemEmailPort,
|
||||
systemEmailAddress: settings.systemEmailAddress,
|
||||
systemEmailPassword: settings.systemEmailPassword,
|
||||
};
|
||||
const res = await networkService.updateAppSettings({ settings: parsedSettings });
|
||||
return res.data;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_CLIENT_HOST;
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
||||
|
||||
const useGetInviteToken = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
27
client/src/Hooks/useBulkMonitors.js
Normal file
27
client/src/Hooks/useBulkMonitors.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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];
|
||||
};
|
||||
@@ -58,7 +58,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
helperText={errors.email ? t(errors.email) : ""}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -23,11 +24,10 @@ const DEMO = import.meta.env.VITE_APP_DEMO;
|
||||
*/
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { authToken } = authState;
|
||||
|
||||
@@ -54,13 +54,17 @@ const Login = () => {
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
const lowerCasedValue = name === idMap["login-email-input"]? value?.toLowerCase()||value : value
|
||||
const lowerCasedValue =
|
||||
name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: lowerCasedValue,
|
||||
}));
|
||||
|
||||
const { error } = credentials.validate({ [name]: lowerCasedValue }, { abortEarly: false });
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: lowerCasedValue },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
@@ -79,8 +83,10 @@ const Login = () => {
|
||||
{ abortEarly: false }
|
||||
);
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
createToast({ body: error.details[0].message });
|
||||
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);
|
||||
}
|
||||
@@ -97,15 +103,15 @@ const Login = () => {
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
? (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: "Welcome back! You're successfully logged in.",
|
||||
body: t("welcomeBack"),
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
|
||||
@@ -15,7 +15,7 @@ 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 } from "react-i18next";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
/**
|
||||
* Displays the initial landing page.
|
||||
*
|
||||
@@ -67,42 +67,47 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
{t("authRegisterBySigningUp")}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
<Trans
|
||||
i18nKey="authRegisterBySigningUp"
|
||||
components={{
|
||||
a1: (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
a2: (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("authRegisterTerms")}
|
||||
</Typography>{" "}
|
||||
{t("and")}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("authRegisterPrivacy")}
|
||||
</Typography>
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -148,8 +153,8 @@ const Register = ({ isSuperAdmin }) => {
|
||||
try {
|
||||
const res = await networkService.verifyInvitationToken(token);
|
||||
const invite = res.data.data;
|
||||
const { role, email, teamId } = invite;
|
||||
setForm({ ...form, email, role, teamId });
|
||||
const { email } = invite;
|
||||
setForm({ ...form, email });
|
||||
} catch (error) {
|
||||
navigate("/register", { replace: true });
|
||||
}
|
||||
@@ -259,7 +264,8 @@ const Register = ({ isSuperAdmin }) => {
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
const lowerCasedValue = name === idMap["register-email-input"]? value?.toLowerCase() || value : value
|
||||
const lowerCasedValue =
|
||||
name === idMap["register-email-input"] ? value?.toLowerCase() || value : value;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: lowerCasedValue,
|
||||
@@ -381,7 +387,7 @@ const Register = ({ isSuperAdmin }) => {
|
||||
}}
|
||||
sx={{ userSelect: "none", color: theme.palette.accent.main }}
|
||||
>
|
||||
{t("authLoginTitle")}
|
||||
{t("authRegisterLoginLink")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -64,7 +64,7 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
|
||||
>
|
||||
<TextInput
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
label={t("authRegisterFirstName")}
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
@@ -76,7 +76,7 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
|
||||
/>
|
||||
<TextInput
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
label={t("authRegisterLastName")}
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
|
||||
@@ -64,7 +64,7 @@ function StepThree({ onSubmit, onBack }) {
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("commonPassword")}
|
||||
label={t("authLoginEnterPassword")}
|
||||
isRequired={true}
|
||||
placeholder={t("createAPassword")}
|
||||
autoComplete="current-password"
|
||||
|
||||
@@ -60,7 +60,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
|
||||
<TextInput
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label={t("commonEmail")}
|
||||
label={t("authRegisterEmail")}
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
@@ -68,7 +68,11 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
helperText={errors.email && (
|
||||
errors.email.includes("required") ? t("authRegisterEmailRequired") :
|
||||
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") :
|
||||
errors.email
|
||||
)}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
// Utils
|
||||
const { formatBytes } = useHardwareUtils();
|
||||
const { statusStyles, determineState } = useUtils();
|
||||
const { determineState } = useUtils();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { stats, uptimePercentage } = monitor ?? {};
|
||||
@@ -41,7 +41,8 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatBox
|
||||
sx={statusStyles[determineState(monitor)]}
|
||||
gradient={true}
|
||||
status={determineState(monitor)}
|
||||
heading={t("status")}
|
||||
subHeading={determineState(monitor)}
|
||||
/>
|
||||
|
||||
@@ -52,7 +52,12 @@ const Filter = ({
|
||||
}, [selectedStatus]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
m: theme.spacing(2),
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<FilterHeader
|
||||
header={t("status")}
|
||||
options={statusOptions}
|
||||
|
||||
@@ -75,6 +75,7 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={openMenu}
|
||||
disabled={!isAdmin}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
|
||||
@@ -94,19 +94,21 @@ const InfrastructureMonitors = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
isLoading={isLoading}
|
||||
path="/infrastructure/create"
|
||||
/>
|
||||
<MonitorCountHeader
|
||||
shouldRender={!isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
shouldRender={!isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
</Stack>
|
||||
<MonitorsTable
|
||||
shouldRender={!isLoading}
|
||||
monitors={monitors}
|
||||
|
||||
@@ -62,7 +62,7 @@ const PageSpeed = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
isLoading={isLoading}
|
||||
path="/pagespeed/create"
|
||||
/>
|
||||
<MonitorCountHeader
|
||||
|
||||
@@ -6,10 +6,10 @@ import Select from "../../Components/Inputs/Select";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import {
|
||||
WalletMultiButton,
|
||||
WalletDisconnectButton,
|
||||
} from "@solana/wallet-adapter-react-ui";
|
||||
// import {
|
||||
// WalletMultiButton,
|
||||
// WalletDisconnectButton,
|
||||
// } from "@solana/wallet-adapter-react-ui";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -123,31 +123,17 @@ const Settings = () => {
|
||||
ttl: form.ttl,
|
||||
});
|
||||
const updatedUser = { ...user, checkTTL: form.ttl };
|
||||
const action = await dispatch(update({ localData: updatedUser }));
|
||||
const settingsAction = await dispatch(
|
||||
updateAppSettings({ settings: { language: language } })
|
||||
);
|
||||
const [userAction, settingsAction] = await Promise.all([
|
||||
dispatch(update({ localData: updatedUser })),
|
||||
dispatch(updateAppSettings({ settings: { language: language } })),
|
||||
]);
|
||||
|
||||
if (action.payload.success && settingsAction.payload.success) {
|
||||
createToast({
|
||||
body: t("settingsSuccessSaved"),
|
||||
});
|
||||
if (userAction.payload.success && settingsAction.payload.success) {
|
||||
createToast({ body: t("settingsSuccessSaved") });
|
||||
} else {
|
||||
if (action.payload) {
|
||||
console.log(action.payload);
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
throw new Error("Failed to save settings");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createToast({ body: t("settingsFailedToSave") });
|
||||
} finally {
|
||||
setChecksIsLoading(false);
|
||||
@@ -268,7 +254,7 @@ const Settings = () => {
|
||||
></Select>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{isAdmin && (
|
||||
{/* {isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
|
||||
@@ -290,8 +276,8 @@ const Settings = () => {
|
||||
: t("settingsDisabled")}
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
)} */}
|
||||
{/* {isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsWallet")}</Typography>
|
||||
@@ -316,7 +302,7 @@ const Settings = () => {
|
||||
</Stack>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)}
|
||||
)} */}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
@@ -362,14 +348,15 @@ const Settings = () => {
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsDemoMonitorsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<>
|
||||
{/* Demo Monitors Section */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsDemoMonitorsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsAddDemoMonitors")}</Typography>
|
||||
<Button
|
||||
@@ -382,6 +369,16 @@ const Settings = () => {
|
||||
{t("settingsAddDemoMonitorsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
|
||||
{/* System Reset Section */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsSystemReset")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsSystemResetDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
|
||||
<Button
|
||||
@@ -396,17 +393,17 @@ const Settings = () => {
|
||||
{t("settingsRemoveAllMonitorsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Dialog
|
||||
open={isOpen.deleteMonitors}
|
||||
theme={theme}
|
||||
title={t("settingsRemoveAllMonitorsDialogTitle")}
|
||||
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
|
||||
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
|
||||
onConfirm={handleDeleteAllMonitors}
|
||||
isLoading={isLoading || authIsLoading || checksIsLoading}
|
||||
/>
|
||||
</ConfigBox>
|
||||
<Dialog
|
||||
open={isOpen.deleteMonitors}
|
||||
theme={theme}
|
||||
title={t("settingsRemoveAllMonitorsDialogTitle")}
|
||||
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
|
||||
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
|
||||
onConfirm={handleDeleteAllMonitors}
|
||||
isLoading={isLoading || authIsLoading || checksIsLoading}
|
||||
/>
|
||||
</ConfigBox>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ConfigBox>
|
||||
|
||||
@@ -5,7 +5,7 @@ import ConfigBox from "../../../../../Components/ConfigBox";
|
||||
import Checkbox from "../../../../../Components/Inputs/Checkbox";
|
||||
import TextInput from "../../../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../../../Components/Inputs/Select";
|
||||
import ImageField from "../../../../../Components/Inputs/Image";
|
||||
import ImageUpload from "../../../../../Components/Inputs/ImageUpload";
|
||||
import ColorPicker from "../../../../../Components/Inputs/ColorPicker";
|
||||
import Progress from "../Progress";
|
||||
|
||||
@@ -106,11 +106,10 @@ const TabSettings = ({
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<ImageField
|
||||
id="logo"
|
||||
<ImageUpload
|
||||
src={form?.logo?.src}
|
||||
isRound={false}
|
||||
onChange={handleImageChange}
|
||||
previewIsRound={false}
|
||||
/>
|
||||
<Progress
|
||||
isLoading={progress.isLoading}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Stack, Button, Typography } from "@mui/material";
|
||||
import Tabs from "./Components/Tabs";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
import SkeletonLayout from "./Components/Skeleton";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx";
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
@@ -59,19 +58,6 @@ const CreateStatusPage = () => {
|
||||
|
||||
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
|
||||
useStatusPageFetch(isCreate, url);
|
||||
|
||||
// Breadcrumbs
|
||||
const crumbs = [
|
||||
{ name: t("statusBreadCrumbsStatusPages"), path: "/status" },
|
||||
];
|
||||
if (isCreate) {
|
||||
crumbs.push({ name: t("statusBreadCrumbsCreate"), path: "/status/uptime/create" });
|
||||
} else {
|
||||
crumbs.push(
|
||||
{ name: t("statusBreadCrumbsDetails"), path: `/status/uptime/${statusPage?.url}` },
|
||||
{ name: t("configure"), path: `/status/uptime/configure/${statusPage?.url}` }
|
||||
);
|
||||
}
|
||||
|
||||
// Handlers
|
||||
const handleFormChange = (e) => {
|
||||
@@ -101,30 +87,31 @@ const CreateStatusPage = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleImageChange = useCallback((event) => {
|
||||
const img = event.target?.files?.[0];
|
||||
const newLogo = {
|
||||
src: URL.createObjectURL(img),
|
||||
name: img.name,
|
||||
type: img.type,
|
||||
size: img.size,
|
||||
};
|
||||
const handleImageChange = useCallback((fileObj) => {
|
||||
if (!fileObj || !fileObj.file) return;
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: newLogo,
|
||||
...prev,
|
||||
logo: {
|
||||
src: fileObj.src,
|
||||
name: fileObj.name,
|
||||
type: fileObj.file.type,
|
||||
size: fileObj.file.size,
|
||||
},
|
||||
}));
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
}, 120);
|
||||
}, []);
|
||||
|
||||
|
||||
const removeLogo = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
@@ -232,7 +219,6 @@ const CreateStatusPage = () => {
|
||||
// Load fields
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={crumbs} />
|
||||
<Tabs
|
||||
form={form}
|
||||
errors={errors}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import Image from "../../../../../Components/Image";
|
||||
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
|
||||
import ThemeSwitch from "../../../../../Components/ThemeSwitch";
|
||||
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -86,6 +87,8 @@ const ControlsHeader = ({
|
||||
type = "uptime",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const publicUrl = `/status/uptime/public/${url}`;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@@ -118,6 +121,26 @@ const ControlsHeader = ({
|
||||
>
|
||||
{statusPage?.companyName}
|
||||
</Typography>
|
||||
{statusPage?.isPublished && !isPublic && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
onClick={() => {
|
||||
window.open(publicUrl, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography>{t("publicLink")}</Typography>
|
||||
<ArrowOutwardIcon />
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Controls
|
||||
isDeleting={isDeleting}
|
||||
|
||||
@@ -139,6 +139,7 @@ const PublicStatus = () => {
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
url={url}
|
||||
isPublic={isPublic}
|
||||
/>
|
||||
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
|
||||
<StatusBar monitors={monitors} />
|
||||
|
||||
@@ -61,6 +61,7 @@ const StatusPages = () => {
|
||||
label="Create status page"
|
||||
isAdmin={isAdmin}
|
||||
path="/status/uptime/create"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatusPagesTable data={statusPages} />
|
||||
</Stack>
|
||||
|
||||
73
client/src/Pages/Uptime/BulkImport/Upload.jsx
Normal file
73
client/src/Pages/Uptime/BulkImport/Upload.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useRef } from "react";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
|
||||
const theme = useTheme();
|
||||
const [file, setFile] = useState();
|
||||
const [error, setError] = useState("");
|
||||
const inputRef = useRef();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelectFile = () => {
|
||||
inputRef.current.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setError("");
|
||||
const selectedFile = e.target.files[0];
|
||||
|
||||
// Basic file validation
|
||||
if (!selectedFile) return;
|
||||
|
||||
if (!selectedFile.name.endsWith('.csv')) {
|
||||
setError(t("bulkImport.invalidFileType"));
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
onFileSelect(selectedFile); // Pass the file directly to parent
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(1.5)}
|
||||
sx={{ wordBreak: "break-all" }}
|
||||
>
|
||||
{file?.name || t("bulkImport.noFileSelected")}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="div"
|
||||
mb={theme.spacing(1.5)}
|
||||
color={theme.palette.error.main}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSelectFile}
|
||||
>
|
||||
{t("bulkImport.selectFile")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
UploadFile.prototype = {
|
||||
onFileSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UploadFile;
|
||||
112
client/src/Pages/Uptime/BulkImport/index.jsx
Normal file
112
client/src/Pages/Uptime/BulkImport/index.jsx
Normal file
@@ -0,0 +1,112 @@
|
||||
// React, Redux, Router
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
// MUI
|
||||
import { Box, Stack, Typography, Button, Link } from "@mui/material";
|
||||
|
||||
//Components
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import UploadFile from "./Upload";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useBulkMonitors } from "../../../Hooks/useBulkMonitors";
|
||||
|
||||
const BulkImport = () => {
|
||||
const theme = useTheme();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
|
||||
const crumbs = [
|
||||
{ name: t("uptime"), path: "/uptime" },
|
||||
{ name: t("bulkImport.title"), path: `/uptime/bulk-import` },
|
||||
];
|
||||
|
||||
const [createBulkMonitors, hookLoading] = useBulkMonitors();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedFile) {
|
||||
createToast({ body: t("bulkImport.noFileSelected") });
|
||||
return;
|
||||
}
|
||||
|
||||
const [success, data, error] = await createBulkMonitors(selectedFile, user);
|
||||
|
||||
if (success) {
|
||||
// You can use `data` here if needed
|
||||
createToast({ body: t("bulkImport.uploadSuccess") });
|
||||
navigate("/uptime");
|
||||
} else {
|
||||
createToast({ body: error || t("bulkImport.uploadFailed") });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="bulk-import-monitor">
|
||||
<Breadcrumbs list={crumbs} />
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
{t("bulkImport.title")}
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("bulkImport.selectFileTips")}</Typography>
|
||||
<Typography component="p">
|
||||
<Trans
|
||||
i18nKey="bulkImport.selectFileDescription"
|
||||
components={{
|
||||
template: (
|
||||
<Link
|
||||
color="info"
|
||||
download
|
||||
href="/bulk_import_monitors_template.csv"
|
||||
/>
|
||||
),
|
||||
sample: (
|
||||
<Link
|
||||
color="info"
|
||||
download
|
||||
href="/bulk_import_monitors_sample.csv"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<UploadFile onFileSelect={(file) => setSelectedFile(file)} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSubmit}
|
||||
disabled={hookLoading}
|
||||
loading={hookLoading}
|
||||
>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkImport;
|
||||
@@ -25,6 +25,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
@@ -196,6 +197,18 @@ const Configure = () => {
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
|
||||
// Notification modal state
|
||||
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
|
||||
|
||||
const handleOpenNotificationModal = () => {
|
||||
setIsNotificationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleClosenNotificationModal = () => {
|
||||
setIsNotificationModalOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
@@ -371,6 +384,17 @@ const Configure = () => {
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
id="monitor-port"
|
||||
label={t("portToMonitor")}
|
||||
placeholder="5173"
|
||||
value={monitor.port || ""}
|
||||
onChange={(event) => handleChange(event, "port")}
|
||||
error={errors["port"] ? true : false}
|
||||
helperText={errors["port"]}
|
||||
hidden={monitor.type !== "port"}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
@@ -413,6 +437,15 @@ const Configure = () => {
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleOpenNotificationModal}
|
||||
>
|
||||
{t("notifications.integrationButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
{/* <Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
@@ -546,6 +579,13 @@ const Configure = () => {
|
||||
onConfirm={handleRemove}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<NotificationIntegrationModal
|
||||
open={isNotificationModalOpen}
|
||||
onClose={handleClosenNotificationModal}
|
||||
monitor={monitor}
|
||||
setMonitor={setMonitor}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -350,7 +350,7 @@ const CreateMonitor = () => {
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
|
||||
<Typography component="p">{t("uptimeCreateSelectURL")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<TextInput
|
||||
|
||||
@@ -61,18 +61,20 @@ const UptimeStatusBoxes = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading="certificate expiry"
|
||||
subHeading={
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={13}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{certificateExpiry}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{monitor?.type === "http" && (
|
||||
<StatBox
|
||||
heading="certificate expiry"
|
||||
subHeading={
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={13}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{certificateExpiry}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -189,8 +189,9 @@ const UptimeMonitors = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
isLoading={isLoading}
|
||||
path="/uptime/create"
|
||||
bulkPath="/uptime/bulk-import"
|
||||
/>
|
||||
<Greeting type="uptime" />
|
||||
<StatusBoxes
|
||||
|
||||
@@ -59,6 +59,7 @@ import ProtectedRoute from "../Components/ProtectedRoute";
|
||||
import ProtectedDistributedUptimeRoute from "../Components/ProtectedDistributedUptimeRoute";
|
||||
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
|
||||
import withAdminCheck from "../Components/HOC/withAdminCheck";
|
||||
import BulkImport from "../Pages/Uptime/BulkImport";
|
||||
|
||||
const Routes = () => {
|
||||
const AdminCheckedRegister = withAdminCheck(AuthRegister);
|
||||
@@ -81,6 +82,11 @@ const Routes = () => {
|
||||
element={<Uptime />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/uptime/bulk-import"
|
||||
element={<BulkImport />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/uptime/create/:monitorId?"
|
||||
element={<UptimeCreate />}
|
||||
@@ -93,16 +99,16 @@ const Routes = () => {
|
||||
path="/uptime/configure/:monitorId/"
|
||||
element={<UptimeConfigure />}
|
||||
/>
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/distributed-uptime"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeMonitors />{" "}
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/distributed-uptime/create"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
@@ -117,15 +123,15 @@ const Routes = () => {
|
||||
<CreateDistributedUptime />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
/> */}
|
||||
{/* <Route
|
||||
path="/distributed-uptime/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeDetails />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="pagespeed"
|
||||
@@ -151,9 +157,9 @@ const Routes = () => {
|
||||
path="infrastructure/create"
|
||||
element={<InfrastructureCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="/infrastructure/configure/:monitorId"
|
||||
element={<InfrastructureCreate />}
|
||||
<Route
|
||||
path="/infrastructure/configure/:monitorId"
|
||||
element={<InfrastructureCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/:monitorId"
|
||||
@@ -174,42 +180,42 @@ const Routes = () => {
|
||||
element={<Status />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/:url"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="status/uptime/create"
|
||||
element={<CreateStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/create/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="status/uptime/configure/:url"
|
||||
element={<CreateStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/configure/:url"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="integrations"
|
||||
@@ -277,10 +283,10 @@ const Routes = () => {
|
||||
path="/status/uptime/public/:url"
|
||||
element={<Status />}
|
||||
/>
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/public/:url"
|
||||
element={<DistributedUptimeStatus />}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="/server-unreachable"
|
||||
|
||||
@@ -32,7 +32,7 @@ class NetworkService {
|
||||
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Accept-Language": currentLanguage,
|
||||
"Accept-Language": currentLanguage === "gb" ? "en" : currentLanguage,
|
||||
...config.headers,
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ class NetworkService {
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
|
||||
if (error.response && error.response.status === 401) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
@@ -275,6 +276,7 @@ class NetworkService {
|
||||
matchMethod: monitor.matchMethod,
|
||||
expectedValue: monitor.expectedValue,
|
||||
jsonPath: monitor.jsonPath,
|
||||
...(monitor.type === "port" && { port: monitor.port }),
|
||||
};
|
||||
return this.axiosInstance.put(`/monitors/${monitorId}`, payload, {
|
||||
headers: {
|
||||
@@ -931,7 +933,7 @@ class NetworkService {
|
||||
onOpen?.();
|
||||
};
|
||||
|
||||
this.eventSource.addEventListener("open", (e) => {});
|
||||
this.eventSource.addEventListener("open", (e) => { });
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -1097,6 +1099,19 @@ class NetworkService {
|
||||
return this.axiosInstance.delete(`/status-page/${encodedUrl}`, {});
|
||||
}
|
||||
|
||||
// ************************************
|
||||
// Create bulk monitors
|
||||
// ************************************
|
||||
|
||||
async createBulkMonitors(formData) {
|
||||
const response = await this.axiosInstance.post(`/monitors/bulk`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ************************************
|
||||
// Fetch monitors with summary by TeamID
|
||||
// ************************************
|
||||
|
||||
@@ -16,7 +16,7 @@ Object.keys(translations).forEach((path) => {
|
||||
});
|
||||
|
||||
const savedLanguage = store.getState()?.ui?.language;
|
||||
const initialLanguage = savedLanguage || primaryLanguage;
|
||||
const initialLanguage = savedLanguage;
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
|
||||
@@ -7,18 +7,11 @@ const nameSchema = joi
|
||||
.string()
|
||||
.max(50)
|
||||
.trim()
|
||||
.pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s']+$/u, {
|
||||
name: "name.containsLetterOrSymbol",
|
||||
})
|
||||
.pattern(/^[\p{L}\p{Sc}\s']+$/u, {
|
||||
name: "name.validCharacters",
|
||||
})
|
||||
.pattern(/^[\p{L}\p{M}''\- ]+$/u)
|
||||
.messages({
|
||||
"string.empty": "Name is required",
|
||||
"string.max": "Name must be less than 50 characters",
|
||||
"string.pattern.name": "Name must contain at least 1 letter or currency symbol",
|
||||
"string.pattern.base.validCharacters":
|
||||
"Can only contain letters, spaces, apostrophes, and currency symbols",
|
||||
"string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
|
||||
});
|
||||
|
||||
const passwordSchema = joi
|
||||
@@ -73,8 +66,8 @@ const credentials = joi.object({
|
||||
return lowercasedValue;
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "Email is required",
|
||||
"string.email": "Must be a valid email address",
|
||||
"string.empty": "authRegisterEmailRequired",
|
||||
"string.email": "authRegisterEmailInvalid",
|
||||
}),
|
||||
password: passwordSchema,
|
||||
newPassword: passwordSchema,
|
||||
@@ -159,7 +152,20 @@ const monitorValidation = joi.object({
|
||||
"string.invalidUrl": "Please enter a valid URL with optional port",
|
||||
"string.pattern.base": "Please enter a valid container ID.",
|
||||
}),
|
||||
port: joi.number(),
|
||||
port: joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.when("type", {
|
||||
is: "port",
|
||||
then: joi.required().messages({
|
||||
"number.base": "Port must be a number.",
|
||||
"number.min": "Port must be at least 1.",
|
||||
"number.max": "Port must be at most 65535.",
|
||||
"any.required": "Port is required for port monitors.",
|
||||
}),
|
||||
otherwise: joi.optional(),
|
||||
}),
|
||||
name: joi.string().trim().max(50).allow("").messages({
|
||||
"string.max": "This field should not exceed the 50 characters limit.",
|
||||
}),
|
||||
@@ -247,7 +253,9 @@ const statusPageValidation = joi.object({
|
||||
});
|
||||
const settingsValidation = joi.object({
|
||||
ttl: joi.number().required().messages({
|
||||
"string.empty": "TTL is required",
|
||||
"string.empty": "Please enter a value",
|
||||
"number.base": "Please enter a valid number",
|
||||
"any.required": "Please enter a value"
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -366,5 +374,5 @@ export {
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
statusPageValidation,
|
||||
logoImageValidation,
|
||||
logoImageValidation
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"registerHere": "Register here",
|
||||
"email": "E-mail",
|
||||
"forgotPassword": "Forgot Password",
|
||||
"password": "password",
|
||||
"signUp": "Sign up",
|
||||
"password": "Password",
|
||||
"signUp": "Sign Up",
|
||||
"submit": "Submit",
|
||||
"title": "Title",
|
||||
"continue": "Continue",
|
||||
@@ -17,9 +17,11 @@
|
||||
"authForgotPasswordTitle": "Forgot password?",
|
||||
"authForgotPasswordResetPassword": "Reset password",
|
||||
"createPassword": "Create your password",
|
||||
"createAPassword": "Create a password",
|
||||
"createAPassword": "Password",
|
||||
"authRegisterAlreadyHaveAccount": "Already have an account?",
|
||||
"authRegisterLoginLink": "Log In",
|
||||
"commonAppName": "Checkmate",
|
||||
"welcomeBack": "Welcome back! You're successfully logged in.",
|
||||
"authLoginEnterEmail": "Enter your email",
|
||||
"authRegisterTitle": "Create an account",
|
||||
"authRegisterStepOneTitle": "Create your account",
|
||||
@@ -42,7 +44,7 @@
|
||||
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
|
||||
"authSetNewPasswordNewPassword": "New password",
|
||||
"authSetNewPasswordConfirmPassword": "Confirm password",
|
||||
"confirmPassword": "Confirm your password",
|
||||
"confirmPassword": "Re-enter password to confirm",
|
||||
"authSetNewPasswordResetPassword": "Reset password",
|
||||
"authSetNewPasswordBackTo": "Back to",
|
||||
"authPasswordMustBeAtLeast": "Must be at least",
|
||||
@@ -53,16 +55,19 @@
|
||||
"authPasswordUpperCharacter": "one upper character",
|
||||
"authPasswordLowerCharacter": "one lower character",
|
||||
"authPasswordConfirmAndPassword": "Confirm password and password",
|
||||
"authPasswordMustMatch": "must match",
|
||||
"authPasswordMustMatch": "Passwords must match",
|
||||
"validationNameRequired": "Please enter your name",
|
||||
"validationNameTooLong": "Name should be less than 50 characters",
|
||||
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
|
||||
"authRegisterCreateAccount": "Create your account to get started",
|
||||
"authRegisterCreateSuperAdminAccount": "Create your Super admin account to get started",
|
||||
"authRegisterSignUpWithEmail": "Sign up with Email",
|
||||
"authRegisterBySigningUp": "By signing up, you agree to our",
|
||||
"authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
|
||||
"authRegisterSignUpWithEmail": "Create super admin account",
|
||||
"authRegisterBySigningUp": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
|
||||
"distributedStatusHeaderText": "Real-time, real-device coverage",
|
||||
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
|
||||
"settingsGeneralSettings": "General settings",
|
||||
"settingsDisplayTimezone": "Display timezone",
|
||||
"settingsDisplayTimezoneDescription": "The timezone of the dashboard you publicly display.",
|
||||
"settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
|
||||
"settingsThemeMode": "Theme Mode",
|
||||
@@ -71,23 +76,25 @@
|
||||
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
|
||||
"settingsEnabled": "Enabled",
|
||||
"settingsDisabled": "Disabled",
|
||||
"settingsHistoryAndMonitoring": "History and monitoring",
|
||||
"settingsHistoryAndMonitoringDescription": "Define here for how long you want to keep the data. You can also remove all past data.",
|
||||
"settingsHistoryAndMonitoring": "History of monitoring",
|
||||
"settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
|
||||
"settingsTTLLabel": "The days you want to keep monitoring history.",
|
||||
"settingsTTLOptionalLabel": "0 for infinite",
|
||||
"settingsClearAllStats": "Clear all stats. This is irreversible.",
|
||||
"settingsClearAllStatsButton": "Clear all stats",
|
||||
"settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
|
||||
"settingsClearAllStatsDialogDescription": "Once deleted, your monitors cannot be retrieved.",
|
||||
"settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
|
||||
"settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
|
||||
"settingsDemoMonitors": "Demo monitors",
|
||||
"settingsDemoMonitorsDescription": "Here you can add and remove demo monitors.",
|
||||
"settingsAddDemoMonitors": "Add demo monitors",
|
||||
"settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
|
||||
"settingsAddDemoMonitors": "Adding demo monitors",
|
||||
"settingsAddDemoMonitorsButton": "Add demo monitors",
|
||||
"settingsRemoveAllMonitors": "Remove all monitors",
|
||||
"settingsSystemReset": "System reset",
|
||||
"settingsSystemResetDescription": "Remove all monitors from your system.",
|
||||
"settingsRemoveAllMonitors": "Removing all monitors",
|
||||
"settingsRemoveAllMonitorsButton": "Remove all monitors",
|
||||
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Yes, clear all monitors",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
|
||||
"settingsWallet": "Wallet",
|
||||
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
|
||||
"settingsAbout": "About",
|
||||
@@ -115,12 +122,12 @@
|
||||
"http": "HTTP",
|
||||
"monitor": "monitor",
|
||||
"aboutus": "About Us",
|
||||
"signUP": "Sign Up",
|
||||
|
||||
"now": "Now",
|
||||
"delete": "Delete",
|
||||
"configure": "Configure",
|
||||
"networkError": "Network error",
|
||||
"responseTime": "Response time:",
|
||||
"responseTime": "Response time",
|
||||
"ms": "ms",
|
||||
"bar": "Bar",
|
||||
"area": "Area",
|
||||
@@ -388,5 +395,41 @@
|
||||
"inviteNoTokenFound": "No invite token found",
|
||||
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key. Without it, the PageSpeed monitor won't function.",
|
||||
"pageSpeedLearnMoreLink": "Click here to learn",
|
||||
"pageSpeedAddApiKey": "how to add your API key."
|
||||
"pageSpeedAddApiKey": "how to add your API key.",
|
||||
"update": "Update",
|
||||
"invalidFileFormat": "Unsupported file format!",
|
||||
"invalidFileSize": "File size is too large!",
|
||||
"ClickUpload": "Click to upload",
|
||||
"DragandDrop": "drag and drop",
|
||||
"MaxSize": "Maximum Size",
|
||||
"SupportedFormats": "Supported formats",
|
||||
"FirstName": "Name",
|
||||
"LastName": "Surname",
|
||||
"EmailDescriptionText": "Your current email—it cannot be changed.",
|
||||
"YourPhoto": "Profile photo",
|
||||
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
|
||||
"save": "Save",
|
||||
"DeleteAccountTitle": "Remove account",
|
||||
"DeleteAccountButton": "Remove account",
|
||||
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
|
||||
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
|
||||
"DeleteWarningTitle": "Really remove this account?",
|
||||
"authRegisterFirstName": "Name",
|
||||
"authRegisterLastName": "Surname",
|
||||
"authRegisterEmail": "Email",
|
||||
"authRegisterEmailRequired": "To continue, please enter your email address",
|
||||
"authRegisterEmailInvalid": "Please enter a valid email address",
|
||||
"bulkImport": {
|
||||
"title": "Bulk Import",
|
||||
"selectFileTips": "Select CSV file to upload",
|
||||
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
|
||||
"selectFile": "Select File",
|
||||
"parsingFailed": "Parsing failed",
|
||||
"uploadSuccess": "Monitors created successfully!",
|
||||
"validationFailed": "Validation failed",
|
||||
"noFileSelected": "No file selected",
|
||||
"fallbackPage": "Import a file to upload a list of servers in bulk"
|
||||
},
|
||||
"publicLink": "Public link"
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"registerHere": "Зарегистрироваться здесь",
|
||||
"email": "Почта",
|
||||
"forgotPassword": "Забыли пароль",
|
||||
"password": "пароль",
|
||||
"password": "Пароль",
|
||||
"signUp": "Зарегистрироваться",
|
||||
"submit": "Подтвердить",
|
||||
"title": "Название",
|
||||
@@ -17,9 +17,11 @@
|
||||
"authForgotPasswordTitle": "Забыли пароль?",
|
||||
"authForgotPasswordResetPassword": "Сбросить пароль",
|
||||
"createPassword": "Создайте свой пароль",
|
||||
"createAPassword": "Создайте пароль",
|
||||
"createAPassword": "Пароль",
|
||||
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
|
||||
"authRegisterLoginLink": "Войти",
|
||||
"commonAppName": "Checkmate",
|
||||
"welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
|
||||
"authLoginEnterEmail": "Введите свой email",
|
||||
"authRegisterTitle": "Создать аккаунт",
|
||||
"authRegisterStepOneTitle": "Создайте свой аккаут",
|
||||
@@ -42,7 +44,8 @@
|
||||
"authSetNewPasswordDescription": "Ваш новый пароль должен отличаться от ранее использованных паролей.",
|
||||
"authSetNewPasswordNewPassword": "Новый пароль",
|
||||
"authSetNewPasswordConfirmPassword": "Подтвердите пароль",
|
||||
"confirmPassword": "Подтвердите ваш пароль",
|
||||
"confirmPassword": "Введите пароль еще раз для подтверждения",
|
||||
"confirmNewPasswordPlaceholder": "Подтвердите ваш новый пароль",
|
||||
"authSetNewPasswordResetPassword": "Сбросить пароль",
|
||||
"authSetNewPasswordBackTo": "Назад к",
|
||||
"authPasswordMustBeAtLeast": "Должно быть как минимум",
|
||||
@@ -53,16 +56,21 @@
|
||||
"authPasswordUpperCharacter": "один верхний символ",
|
||||
"authPasswordLowerCharacter": "один нижний символ",
|
||||
"authPasswordConfirmAndPassword": "Подтвердите пароль и пароль",
|
||||
"authPasswordMustMatch": "должен совпадать",
|
||||
"authPasswordMustMatch": "Пароли должны совпадать",
|
||||
"authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
|
||||
"authRegisterCreateSuperAdminAccount": "Создайте учетную запись суперадминистратора, чтобы начать работу",
|
||||
"authRegisterSignUpWithEmail": "Зарегистрироваться по электронной почте",
|
||||
"authRegisterBySigningUp": "Регистрируясь, вы соглашаетесь с нашими",
|
||||
"authRegisterSignUpWithEmail": "Создать учетную запись суперадминистратора",
|
||||
"authRegisterBySigningUp": "Создавая учетную запись, вы соглашаетесь с нашими <a1>Условиями использования</a1> и <a2>Политикой конфиденциальности</a2>.",
|
||||
"authRegisterFirstName": "Имя",
|
||||
"authRegisterLastName": "Фамилия",
|
||||
"authRegisterEmail": "Эл. почта",
|
||||
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
|
||||
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
|
||||
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
|
||||
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
|
||||
"settingsGeneralSettings": "Общие настройки",
|
||||
"settingsDisplayTimezone": "Отображать часовой пояс",
|
||||
"settingsDisplayTimezoneDescription": "Часовой пояс панели мониторинга, которую вы публично отображаете.",
|
||||
"settingsDisplayTimezoneDescription": "Выберите часовой пояс, используемый для отображения дат и времени в приложении.",
|
||||
"settingsAppearance": "Внешний вид",
|
||||
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
|
||||
"settingsThemeMode": "Тема",
|
||||
@@ -71,23 +79,25 @@
|
||||
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
|
||||
"settingsEnabled": "Включено",
|
||||
"settingsDisabled": "Выключено",
|
||||
"settingsHistoryAndMonitoring": "История и мониторинг",
|
||||
"settingsHistoryAndMonitoringDescription": "Определите здесь, как долго вы хотите хранить данные. Вы также можете удалить все прошлые данные.",
|
||||
"settingsHistoryAndMonitoring": "История мониторинга",
|
||||
"settingsHistoryAndMonitoringDescription": "Определите, как долго вы хотите хранить исторические данные. Вы также можете очистить все существующие данные.",
|
||||
"settingsTTLLabel": "Дни, за которыми вы хотите следить.",
|
||||
"settingsTTLOptionalLabel": "0 для бесконечности",
|
||||
"settingsClearAllStats": "Очистить всю статистику. Это необратимо.",
|
||||
"settingsClearAllStatsButton": "Очистить всю статистику",
|
||||
"settingsClearAllStatsDialogTitle": "Хотите очистить всю статистику?",
|
||||
"settingsClearAllStatsDialogDescription": "После удаления ваши мониторы не могут быть восстановлены.",
|
||||
"settingsClearAllStatsDialogDescription": "После удаления история мониторинга и статистика не могут быть восстановлены.",
|
||||
"settingsClearAllStatsDialogConfirm": "Да, очистить всю статистику",
|
||||
"settingsDemoMonitors": "Демо мониторы",
|
||||
"settingsDemoMonitorsDescription": "Здесь вы можете добавлять и удалять демонстрационные мониторы.",
|
||||
"settingsAddDemoMonitors": "Добавьте демонстрационные мониторы",
|
||||
"settingsDemoMonitorsDescription": "Добавьте примеры мониторов для демонстрации.",
|
||||
"settingsAddDemoMonitors": "Добавление демонстрационных мониторов",
|
||||
"settingsAddDemoMonitorsButton": "Добавьте демонстрационные мониторы",
|
||||
"settingsRemoveAllMonitors": "Удалить все демонстрационные мониторы",
|
||||
"settingsRemoveAllMonitorsButton": "Удалить все демонстрационные мониторы",
|
||||
"settingsSystemReset": "Сброс системы",
|
||||
"settingsSystemResetDescription": "Удалить все мониторы из вашей системы.",
|
||||
"settingsRemoveAllMonitors": "Удаление всех мониторов",
|
||||
"settingsRemoveAllMonitorsButton": "Удалить все мониторы",
|
||||
"settingsRemoveAllMonitorsDialogTitle": "Хотите удалить все мониторы?",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Да, очистить все мониторы",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Да, удалить все мониторы",
|
||||
"settingsWallet": "Кошелёк",
|
||||
"settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
|
||||
"settingsAbout": "О",
|
||||
@@ -97,6 +107,16 @@
|
||||
"settingsFailedToSave": "Не удалось сохранить настройки",
|
||||
"settingsStatsCleared": "Статистика успешно очищена",
|
||||
"settingsFailedToClearStats": "Не удалось очистить статистику",
|
||||
"FirstName": "Имя",
|
||||
"LastName": "Фамилия",
|
||||
"YourPhoto": "Фото профиля",
|
||||
"PhotoDescriptionText": "Это фото будет отображаться на странице вашего профиля.",
|
||||
"EmailDescriptionText": "Ваш текущий email—его нельзя изменить.",
|
||||
"DeleteAccountTitle": "Удалить аккаунт",
|
||||
"DeleteAccountButton": "Удалить аккаунт",
|
||||
"DeleteDescriptionText": "Это удалит аккаунт и все связанные данные с сервера. Это необратимо.",
|
||||
"DeleteAccountWarning": "Удаление аккаунта означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
|
||||
"DeleteWarningTitle": "Действительно удалить этот аккаунт?",
|
||||
"settingsDemoMonitorsAdded": "Успешно добавлены демонстрационные мониторы",
|
||||
"settingsFailedToAddDemoMonitors": "Не удалось добавить демонстрационные мониторы",
|
||||
"settingsMonitorsDeleted": "Успешно удалены все мониторы",
|
||||
@@ -115,12 +135,12 @@
|
||||
"http": "HTTP",
|
||||
"monitor": "монитор",
|
||||
"aboutus": "О Нас",
|
||||
"signUP": "Зарегистрироваться",
|
||||
|
||||
"now": "Сейчас",
|
||||
"delete": "Удалить",
|
||||
"configure": "Настроить",
|
||||
"networkError": "Ошибка сети",
|
||||
"responseTime": "Время ответа:",
|
||||
"responseTime": "Время ответа",
|
||||
"ms": "мс",
|
||||
"bar": "Bar",
|
||||
"area": "Area",
|
||||
@@ -374,4 +394,4 @@
|
||||
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
|
||||
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
|
||||
"pageSpeedAddApiKey": "как добавить ваш ключ API."
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,11 @@
|
||||
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
|
||||
"authForgotPasswordResetPassword": "Parola sıfırla",
|
||||
"createPassword": "Parolanızı oluşturun",
|
||||
"createAPassword": "Bir parola oluşturun",
|
||||
"createAPassword": "Parola",
|
||||
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
|
||||
"authRegisterLoginLink": "Giriş Yap",
|
||||
"commonAppName": "Checkmate",
|
||||
"welcomeBack": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
|
||||
"authLoginEnterEmail": "E-posta adresinizi girin",
|
||||
"authRegisterTitle": "Hesap oluştur",
|
||||
"authRegisterStepOneTitle": "Hesabınızı oluşturun",
|
||||
@@ -42,7 +44,8 @@
|
||||
"authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
|
||||
"authSetNewPasswordNewPassword": "Yeni şifre",
|
||||
"authSetNewPasswordConfirmPassword": "Parolayı onayla",
|
||||
"confirmPassword": "Parolanızı onaylayın",
|
||||
"confirmPassword": "Onaylamak için parolayı tekrar girin",
|
||||
"confirmNewPasswordPlaceholder": "Yeni parolanızı onaylayın",
|
||||
"authSetNewPasswordResetPassword": "Parola sıfırla",
|
||||
"authSetNewPasswordBackTo": "Geri dön",
|
||||
"authPasswordMustBeAtLeast": "En az",
|
||||
@@ -53,7 +56,7 @@
|
||||
"authPasswordUpperCharacter": "bir büyük harf",
|
||||
"authPasswordLowerCharacter": "bir küçük harf",
|
||||
"authPasswordConfirmAndPassword": "Onay şifresi ve şifre",
|
||||
"authPasswordMustMatch": "eşleşmelidir",
|
||||
"authPasswordMustMatch": "Parolalar eşleşmelidir",
|
||||
"authRegisterCreateAccount": "Hesap oluşturmak için devam et",
|
||||
"authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin",
|
||||
"authRegisterSignUpWithEmail": "E-posta ile kayıt ol",
|
||||
@@ -62,7 +65,7 @@
|
||||
"distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin",
|
||||
"settingsGeneralSettings": "Genel ayarlar",
|
||||
"settingsDisplayTimezone": "Görüntüleme saat dilimi",
|
||||
"settingsDisplayTimezoneDescription": "Herkese açık olarak görüntülediğiniz kontrol panelinin saat dilimi.",
|
||||
"settingsDisplayTimezoneDescription": "Uygulama genelinde tarih ve saatlerin görüntülenmesi için kullanılacak saat dilimini seçin.",
|
||||
"settingsAppearance": "Görünüm",
|
||||
"settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin",
|
||||
"settingsThemeMode": "Tema",
|
||||
@@ -71,23 +74,25 @@
|
||||
"settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
|
||||
"settingsEnabled": "Etkin",
|
||||
"settingsDisabled": "Devre dışı",
|
||||
"settingsHistoryAndMonitoring": "Geçmiş ve izleme",
|
||||
"settingsHistoryAndMonitoringDescription": "Verileri ne kadar süreyle saklamak istediğinizi burada tanımlayın. Ayrıca tüm geçmiş verileri kaldırabilirsiniz.",
|
||||
"settingsHistoryAndMonitoring": "İzleme geçmişi",
|
||||
"settingsHistoryAndMonitoringDescription": "Geçmiş verileri ne kadar süre saklamak istediğinizi tanımlayın. Ayrıca mevcut tüm verileri temizleyebilirsiniz.",
|
||||
"settingsTTLLabel": "İzleme geçmişini saklamak istediğiniz gün sayısı.",
|
||||
"settingsTTLOptionalLabel": "Sınırsız için 0",
|
||||
"settingsClearAllStats": "Tüm istatistikleri temizle. Bu geri alınamaz.",
|
||||
"settingsClearAllStatsButton": "Tüm istatistikleri temizle",
|
||||
"settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?",
|
||||
"settingsClearAllStatsDialogDescription": "Silindikten sonra, monitörleriniz geri alınamaz.",
|
||||
"settingsClearAllStatsDialogDescription": "Kaldırıldıktan sonra, izleme geçmişi ve istatistikleri geri alınamaz.",
|
||||
"settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle",
|
||||
"settingsDemoMonitors": "Demo monitörler",
|
||||
"settingsDemoMonitorsDescription": "Burada demo monitörler ekleyebilir ve kaldırabilirsiniz.",
|
||||
"settingsAddDemoMonitors": "Demo monitörler ekle",
|
||||
"settingsDemoMonitorsDescription": "Gösterim amaçlı örnek monitörler ekleyin.",
|
||||
"settingsAddDemoMonitors": "Demo monitörler ekleniyor",
|
||||
"settingsAddDemoMonitorsButton": "Demo monitörler ekle",
|
||||
"settingsRemoveAllMonitors": "Tüm monitörleri kaldır",
|
||||
"settingsSystemReset": "Sistem sıfırlama",
|
||||
"settingsSystemResetDescription": "Sisteminizden tüm monitörleri kaldırın.",
|
||||
"settingsRemoveAllMonitors": "Tüm monitörler kaldırılıyor",
|
||||
"settingsRemoveAllMonitorsButton": "Tüm monitörleri kaldır",
|
||||
"settingsRemoveAllMonitorsDialogTitle": "Tüm monitörleri kaldırmak istiyor musunuz?",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri temizle",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri kaldır",
|
||||
"settingsWallet": "Cüzdan",
|
||||
"settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.",
|
||||
"settingsAbout": "Hakkında",
|
||||
@@ -115,12 +120,12 @@
|
||||
"http": "HTTP",
|
||||
"monitor": "monitör",
|
||||
"aboutus": "Hakkımızda",
|
||||
"signUP": "Hesap Oluştur",
|
||||
|
||||
"now": "Şimdi",
|
||||
"delete": "Sil",
|
||||
"configure": "Yapılandır",
|
||||
"networkError": "Ağ hatası",
|
||||
"responseTime": "Yanıt süresi:",
|
||||
"responseTime": "Yanıt süresi",
|
||||
"ms": "ms",
|
||||
"bar": "Çubuk",
|
||||
"area": "Alan",
|
||||
@@ -388,5 +393,39 @@
|
||||
"inviteNoTokenFound": "",
|
||||
"pageSpeedWarning": "",
|
||||
"pageSpeedLearnMoreLink": "",
|
||||
"pageSpeedAddApiKey": ""
|
||||
"pageSpeedAddApiKey": "",
|
||||
"update": "",
|
||||
"invalidFileFormat": "",
|
||||
"invalidFileSize": "",
|
||||
"ClickUpload": "",
|
||||
"DragandDrop": "",
|
||||
"MaxSize": "",
|
||||
"SupportedFormats": "",
|
||||
"FirstName": "Ad",
|
||||
"LastName": "Soyad",
|
||||
"EmailDescriptionText": "Mevcut e-postanız—değiştirilemez.",
|
||||
"YourPhoto": "Profil fotoğrafı",
|
||||
"PhotoDescriptionText": "Bu fotoğraf profil sayfanızda görüntülenecektir.",
|
||||
"save": "",
|
||||
"DeleteAccountTitle": "Hesabı kaldır",
|
||||
"DeleteAccountButton": "Hesabı kaldır",
|
||||
"DeleteDescriptionText": "Bu, hesabı ve tüm ilişkili verileri sunucudan kaldıracaktır. Bu geri alınamaz.",
|
||||
"DeleteAccountWarning": "Hesabınızı kaldırmak, tekrar oturum açamayacağınız ve tüm verilerinizin kaldırılacağı anlamına gelir. Bu geri alınamaz.",
|
||||
"DeleteWarningTitle": "Bu hesabı gerçekten kaldırmak istiyor musunuz?",
|
||||
"authRegisterFirstName": "Ad",
|
||||
"authRegisterLastName": "Soyad",
|
||||
"authRegisterEmail": "E-posta",
|
||||
"authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
|
||||
"authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin",
|
||||
"bulkImport": {
|
||||
"title": "",
|
||||
"selectFileTips": "",
|
||||
"selectFileDescription": "",
|
||||
"selectFile": "",
|
||||
"parsingFailed": "",
|
||||
"uploadSuccess": "",
|
||||
"validationFailed": "",
|
||||
"noFileSelected": "",
|
||||
"fallbackPage": ""
|
||||
}
|
||||
}
|
||||
|
||||
1
docker/.gitignore
vendored
1
docker/.gitignore
vendored
@@ -7,5 +7,6 @@ dist/mongo/data/*
|
||||
dist/redis/data/*
|
||||
prod/mongo/data/*
|
||||
prod/redis/data/*
|
||||
dist/docker-compose-test.yaml
|
||||
*.env
|
||||
prod/certbot/*
|
||||
@@ -4,23 +4,27 @@
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["uptime_client"]="./docker/dev/client.Dockerfile"
|
||||
["uptime_database_mongo"]="./docker/dev/mongoDB.Dockerfile"
|
||||
["uptime_redis"]="./docker/dev/redis.Dockerfile"
|
||||
["uptime_server"]="./docker/dev/server.Dockerfile"
|
||||
# Define service names and their corresponding Dockerfiles in parallel arrays
|
||||
services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server")
|
||||
dockerfiles=(
|
||||
"./docker/dev/client.Dockerfile"
|
||||
"./docker/dev/mongoDB.Dockerfile"
|
||||
"./docker/dev/redis.Dockerfile"
|
||||
"./docker/dev/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
for i in "${!services[@]}"; do
|
||||
service="${services[$i]}"
|
||||
dockerfile="${dockerfiles[$i]}"
|
||||
|
||||
## Check if the build succeeded
|
||||
docker build -f "$dockerfile" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All images built successfully"
|
||||
echo "All images built successfully"
|
||||
@@ -18,10 +18,13 @@ COPY ./client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ./client .
|
||||
COPY ./client ./
|
||||
|
||||
RUN npm run build-dev
|
||||
RUN npm run build
|
||||
|
||||
RUN npm install -g serve
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
CMD ["serve","-s", "dist", "-l", "5173"]
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -3,8 +3,12 @@ services:
|
||||
image: uptime_client:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "5173:5173"
|
||||
|
||||
- "80:80"
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "http://localhost"
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d/
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
@@ -31,10 +35,17 @@ services:
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
image: uptime_mongo:latest
|
||||
restart: always
|
||||
command: ["mongod", "--quiet"]
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
|
||||
35
docker/dev/nginx/conf.d/default.conf
Executable file
35
docker/dev/nginx/conf.d/default.conf
Executable file
@@ -0,0 +1,35 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
server_name localhost; # Set server name to localhost
|
||||
server_tokens off;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# location /api/ {
|
||||
# proxy_pass http://server:5000/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header Connection '';
|
||||
# chunked_transfer_encoding off;
|
||||
# proxy_buffering off;
|
||||
# proxy_cache off;
|
||||
# }
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
4
docker/dist/build_images.sh
vendored
4
docker/dist/build_images.sh
vendored
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../../../
|
||||
cd ../../
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["bluewaveuptime/uptime_client"]=".docker/dist/client.Dockerfile"
|
||||
["bluewaveuptime/uptime_client"]="./docker/dist/client.Dockerfile"
|
||||
["bluewaveuptime/uptime_database_mongo"]="./docker/dist/mongoDB.Dockerfile"
|
||||
["bluewaveuptime/uptime_redis"]="./docker/dist/redis.Dockerfile"
|
||||
["bluewaveuptime/uptime_server"]="./docker/dist/server.Dockerfile"
|
||||
|
||||
2
docker/dist/client.Dockerfile
vendored
2
docker/dist/client.Dockerfile
vendored
@@ -23,7 +23,7 @@ RUN npm run build
|
||||
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
COPY ./server/docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
|
||||
24
docker/dist/docker-compose.yaml
vendored
24
docker/dist/docker-compose.yaml
vendored
@@ -1,17 +1,17 @@
|
||||
services:
|
||||
client:
|
||||
image: bluewaveuptime/uptime_client:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:frontend-dist
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
UPTIME_APP_CLIENT_HOST: "http://localhost"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
image: bluewaveuptime/uptime_server:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-dist
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
@@ -19,12 +19,13 @@ services:
|
||||
- redis
|
||||
- mongodb
|
||||
environment:
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
|
||||
- REDIS_HOST=redis
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- CLIENT_HOST=http://localhost
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: bluewaveuptime/uptime_redis:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:redis-dist
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -37,10 +38,17 @@ services:
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: bluewaveuptime/uptime_database_mongo:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:mongo-dist
|
||||
restart: always
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
command: ["mongod", "--quiet"]
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
|
||||
16
docker/dist/nginx/conf.d/default.conf
vendored
16
docker/dist/nginx/conf.d/default.conf
vendored
@@ -15,14 +15,14 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
# location /api/ {
|
||||
# proxy_pass http://server:5000/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
services:
|
||||
client:
|
||||
image: uptime_client:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:frontend-demo
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
- ./certbot/www/:/var/www/certbot/:rw
|
||||
- ./certbot/conf/:/etc/letsencrypt/:rw
|
||||
server:
|
||||
image: uptime_server:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-demo
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
@@ -31,8 +31,10 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: uptime_redis:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:redis-demo
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -45,13 +47,20 @@ services:
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
|
||||
restart: always
|
||||
command: ["mongod", "--quiet", "--auth"]
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
|
||||
# - ./mongo/init/init.js:/docker-entrypoint-initdb.d/init.js // No longer needed
|
||||
env_file:
|
||||
- mongo.env
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
|
||||
41
docker/prod/mongo/init/init.js
Normal file
41
docker/prod/mongo/init/init.js
Normal file
@@ -0,0 +1,41 @@
|
||||
function initiateReplicaSet() {
|
||||
try {
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "localhost:27017" }],
|
||||
});
|
||||
} catch (e) {
|
||||
print("Replica set already initiated or error occurred: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
function waitForPrimary() {
|
||||
while (true) {
|
||||
const isMaster = db.isMaster();
|
||||
if (isMaster.ismaster) {
|
||||
print("This node is now PRIMARY.");
|
||||
break;
|
||||
}
|
||||
print("Waiting to become PRIMARY...");
|
||||
sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
function createUser() {
|
||||
db = db.getSiblingDB("uptime_db");
|
||||
|
||||
var username = process.env.USERNAME_ENV_VAR;
|
||||
var password = process.env.PASSWORD_ENV_VAR;
|
||||
|
||||
db.createUser({
|
||||
user: username,
|
||||
pwd: password,
|
||||
roles: [{ role: "readWrite", db: "uptime_db" }],
|
||||
});
|
||||
|
||||
print("User '" + username + "' created successfully");
|
||||
}
|
||||
|
||||
initiateReplicaSet();
|
||||
waitForPrimary();
|
||||
createUser();
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
UPTIME_APP_CLIENT_HOST: "https://checkmate-test.bluewavelabs.ca"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -54,6 +54,13 @@ services:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
|
||||
# - ./mongo/init/02_create_users.js:/docker-entrypoint-initdb.d/02_create_users.js // No longer needed
|
||||
env_file:
|
||||
- mongo.env
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
|
||||
15
docker/staging/mongo/init/01_init_replica_set.js
Normal file
15
docker/staging/mongo/init/01_init_replica_set.js
Normal file
@@ -0,0 +1,15 @@
|
||||
try {
|
||||
const status = rs.status();
|
||||
printjson(status);
|
||||
} catch (e) {
|
||||
if (e.codeName === "NotYetInitialized") {
|
||||
print("Replica set not initialized. Initiating...");
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "mongodb:27017" }],
|
||||
});
|
||||
} else {
|
||||
print("Unexpected error during rs.status():");
|
||||
printjson(e);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
var username = process.env.USERNAME_ENV_VAR;
|
||||
var password = process.env.PASSWORD_ENV_VAR;
|
||||
|
||||
db = db.getSiblingDB("uptime_db");
|
||||
|
||||
db.createUser({
|
||||
user: username,
|
||||
pwd: password,
|
||||
roles: [
|
||||
{
|
||||
role: "readWrite",
|
||||
db: "uptime_db",
|
||||
},
|
||||
],
|
||||
});
|
||||
print("User uptime_user created successfully");
|
||||
@@ -57,9 +57,9 @@ class AuthController {
|
||||
*/
|
||||
registerUser = async (req, res, next) => {
|
||||
try {
|
||||
if(req.body?.email){
|
||||
if (req.body?.email) {
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
}
|
||||
}
|
||||
await registrationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
@@ -68,11 +68,13 @@ class AuthController {
|
||||
}
|
||||
// Create a new user
|
||||
try {
|
||||
const { inviteToken } = req.body;
|
||||
const user = req.body;
|
||||
// If superAdmin exists, a token should be attached to all further register requests
|
||||
const superAdminExists = await this.db.checkSuperadmin(req, res);
|
||||
if (superAdminExists) {
|
||||
await this.db.getInviteTokenAndDelete(inviteToken);
|
||||
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
|
||||
user.role = invitedUser.role;
|
||||
user.teamId = invitedUser.teamId;
|
||||
} else {
|
||||
// This is the first account, create JWT secret to use if one is not supplied by env
|
||||
const jwtSecret = crypto.randomBytes(64).toString("hex");
|
||||
@@ -133,8 +135,8 @@ class AuthController {
|
||||
*/
|
||||
loginUser = async (req, res, next) => {
|
||||
try {
|
||||
if(req.body?.email){
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
if (req.body?.email) {
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
}
|
||||
await loginValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
|
||||
@@ -24,13 +24,15 @@ import axios from "axios";
|
||||
import seedDb from "../db/mongo/utils/seedDb.js";
|
||||
import { seedDistributedTest } from "../db/mongo/utils/seedDb.js";
|
||||
const SERVICE_NAME = "monitorController";
|
||||
import pkg from "papaparse";
|
||||
|
||||
class MonitorController {
|
||||
constructor(db, settingsService, jobQueue, stringService) {
|
||||
constructor(db, settingsService, jobQueue, stringService, emailService) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,56 +245,119 @@ class MonitorController {
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates bulk monitors and adds them to the job queue.
|
||||
* Creates bulk monitors and adds them to the job queue after parsing CSV.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {Object} req.file - The uploaded CSV file.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
|
||||
* @returns {Object} The response object with a success status and message.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
createBulkMonitors = async (req, res, next) => {
|
||||
try {
|
||||
await createMonitorsBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
const { parse } = pkg;
|
||||
|
||||
try {
|
||||
// create monitors
|
||||
const monitors = await this.db.createBulkMonitors(req);
|
||||
// validate the file
|
||||
if (!req.file) {
|
||||
throw new Error("No file uploaded");
|
||||
}
|
||||
|
||||
// create notifications for each monitor
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor, index) => {
|
||||
const notifications = req.body[index].notifications;
|
||||
// Check if the file is a CSV
|
||||
if (!req.file.mimetype.includes("csv")) {
|
||||
throw new Error("File is not a CSV");
|
||||
}
|
||||
|
||||
if (notifications?.length) {
|
||||
monitor.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
return await this.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
await monitor.save();
|
||||
// Validate if the file is empty
|
||||
if (req.file.size === 0) {
|
||||
throw new Error("File is empty");
|
||||
}
|
||||
|
||||
const { userId, teamId } = req.body;
|
||||
|
||||
if (!userId || !teamId) {
|
||||
throw new Error("Missing userId or teamId in form data");
|
||||
}
|
||||
|
||||
// Get file buffer from memory and convert to string
|
||||
const fileData = req.file.buffer.toString("utf-8");
|
||||
|
||||
// Parse the CSV data
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value, header) => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw new Error(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
// Add monitor to job queue
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }) => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw new Error("Error parsing CSV");
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.bulkMonitorsCreate,
|
||||
data: monitors,
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
try {
|
||||
const monitors = await this.db.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor, index) => {
|
||||
const notifications = enrichedData[index].notifications;
|
||||
|
||||
if (notifications?.length) {
|
||||
monitor.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
return await this.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
await monitor.save();
|
||||
}
|
||||
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.bulkMonitorsCreate,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
return next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the endpoint can be resolved
|
||||
* @async
|
||||
@@ -473,18 +538,16 @@ class MonitorController {
|
||||
const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
|
||||
|
||||
// Get notifications from the request body
|
||||
const notifications = req.body.notifications;
|
||||
|
||||
const notifications = req.body.notifications ?? [];
|
||||
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
|
||||
|
||||
await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
|
||||
|
||||
await Promise.all(
|
||||
notifications &&
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = editedMonitor._id;
|
||||
await this.db.createNotification(notification);
|
||||
})
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = editedMonitor._id;
|
||||
await this.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
|
||||
// Delete the old job(editedMonitor has the same ID as the old monitor)
|
||||
@@ -572,6 +635,50 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a test email to verify email delivery functionality.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.to - The email address to send the test email to.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status and the email delivery message ID.
|
||||
* @throws {Error} If there is an error while sending the test email.
|
||||
*/
|
||||
sendTestEmail = async (req, res, next) => {
|
||||
try {
|
||||
const { to } = req.body;
|
||||
|
||||
if (!to || typeof to !== "string") {
|
||||
return res.error({ msg: this.stringService.errorForValidEmailAddress });
|
||||
}
|
||||
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const messageId = await this.emailService.buildAndSendEmail(
|
||||
"testEmailTemplate",
|
||||
context,
|
||||
to,
|
||||
subject
|
||||
);
|
||||
|
||||
if (!messageId) {
|
||||
return res.error({
|
||||
msg: "Failed to send test email.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.sendTestEmail,
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "sendTestEmail"));
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
// **************************
|
||||
// The idea here is to provide a layer of abstraction between the database and whoever is using it.
|
||||
// Instead of directly calling mongoose methods, we can call the methods on the DB object.
|
||||
// If this were Typescript or Java or Golang an interface would be implemented to ensure the methods are available.
|
||||
// But we do the best we can with Javascript.
|
||||
//
|
||||
// If the methods are consistent all we have to do to swap out one DB for another is simply change the import.
|
||||
//
|
||||
// Example:
|
||||
// We start with the fake DB:
|
||||
//
|
||||
// const db = require("../db/FakeDb");
|
||||
// const monitors = await db.getAllMonitors();
|
||||
//
|
||||
// And when we want to swtich to a real DB, all we have to do is swap the import
|
||||
//
|
||||
// const db = require("../db/MongoDb");
|
||||
// const monitors = await db.getAllMonitors();
|
||||
//
|
||||
// The rest of the code is the same, as all the `db` methods are standardized.
|
||||
// **************************
|
||||
|
||||
const Monitor = require("./models/Monitor");
|
||||
const UserModel = require("./models/User");
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
let FAKE_MONITOR_DATA = [];
|
||||
const USERS = [];
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
await console.log("Connected to FakeDB");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const insertUser = async (req, res) => {
|
||||
try {
|
||||
const newUser = new UserModel({ ...req.body });
|
||||
const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
|
||||
newUser.password = await bcrypt.hash(newUser.password, salt); // hash is also async, need to eitehr await or use hashSync
|
||||
USERS.push(newUser);
|
||||
const userToReturn = { ...newUser._doc };
|
||||
delete userToReturn.password;
|
||||
return userToReturn;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getUserByEmail = async (req, res) => {
|
||||
const email = req.body.email;
|
||||
try {
|
||||
const idx = USERS.findIndex((user) => {
|
||||
return user.email === email;
|
||||
});
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
}
|
||||
return USERS[idx];
|
||||
} catch (error) {
|
||||
throw new Error(`User with email ${email} not found`);
|
||||
}
|
||||
};
|
||||
|
||||
const getAllMonitors = async () => {
|
||||
return FAKE_MONITOR_DATA;
|
||||
};
|
||||
|
||||
const getMonitorById = async (monitorId) => {
|
||||
const idx = FAKE_MONITOR_DATA.findIndex((monitor) => {
|
||||
return monitor.id === monitorId;
|
||||
});
|
||||
if (idx === -1) {
|
||||
throw new Error(`Monitor with id ${monitorId} not found`);
|
||||
}
|
||||
return FAKE_MONITOR_DATA[idx];
|
||||
};
|
||||
|
||||
const getMonitorsByUserId = async (userId) => {
|
||||
const userMonitors = FAKE_MONITOR_DATA.filter((monitor) => {
|
||||
return monitor.userId === userId;
|
||||
});
|
||||
|
||||
if (userMonitors.length === 0) {
|
||||
throw new Error(`Monitors for user ${userId} not found`);
|
||||
}
|
||||
return userMonitors;
|
||||
};
|
||||
|
||||
const createMonitor = async (req, res) => {
|
||||
const monitor = new Monitor(req.body);
|
||||
monitor.createdAt = Date.now();
|
||||
monitor.updatedAt = Date.now();
|
||||
FAKE_MONITOR_DATA.push(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
const deleteMonitor = async (req, res) => {
|
||||
const monitorId = req.params.monitorId;
|
||||
try {
|
||||
const monitor = getMonitorById(monitorId);
|
||||
FAKE_MONITOR_DATA = FAKE_MONITOR_DATA.filter((monitor) => {
|
||||
return monitor.id !== monitorId;
|
||||
});
|
||||
return monitor;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const editMonitor = async (req, res) => {
|
||||
const monitorId = req.params.monitorId;
|
||||
const idx = FAKE_MONITOR_DATA.findIndex((monitor) => {
|
||||
return monitor._id.toString() === monitorId;
|
||||
});
|
||||
const oldMonitor = FAKE_MONITOR_DATA[idx];
|
||||
const editedMonitor = new Monitor({ ...req.body });
|
||||
editedMonitor._id = oldMonitor._id;
|
||||
editedMonitor.userId = oldMonitor.userId;
|
||||
editedMonitor.updatedAt = Date.now();
|
||||
editedMonitor.createdAt = oldMonitor.createdAt;
|
||||
FAKE_MONITOR_DATA[idx] = editedMonitor;
|
||||
return FAKE_MONITOR_DATA[idx];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
getAllMonitors,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
editMonitor,
|
||||
};
|
||||
@@ -7,10 +7,6 @@ const AppSettingsSchema = mongoose.Schema(
|
||||
required: true,
|
||||
default: "http://localhost:5000/api/v1",
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
logLevel: {
|
||||
type: String,
|
||||
default: "debug",
|
||||
|
||||
@@ -77,7 +77,6 @@ CheckSchema.index({ updatedAt: 1 });
|
||||
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
|
||||
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
|
||||
CheckSchema.index({ teamId: 1, updatedAt: -1 });
|
||||
CheckSchema.index({ teamId: 1 });
|
||||
|
||||
export default mongoose.model("Check", CheckSchema);
|
||||
export { BaseCheckSchema };
|
||||
|
||||
@@ -15,6 +15,7 @@ const InviteTokenSchema = mongoose.Schema(
|
||||
role: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: ["user"],
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
|
||||
@@ -39,6 +39,10 @@ const MonitorSchema = mongoose.Schema(
|
||||
"distributed_test",
|
||||
],
|
||||
},
|
||||
ignoreTlsErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
jsonPath: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mongoose from "mongoose";
|
||||
import bcrypt from "bcrypt";
|
||||
import bcrypt from "bcryptjs";
|
||||
import logger from "../../utils/logger.js";
|
||||
|
||||
const UserSchema = mongoose.Schema(
|
||||
@@ -55,20 +55,20 @@ const UserSchema = mongoose.Schema(
|
||||
}
|
||||
);
|
||||
|
||||
UserSchema.pre("save", async function (next) {
|
||||
UserSchema.pre("save", function (next) {
|
||||
if (!this.isModified("password")) {
|
||||
next();
|
||||
return next();
|
||||
}
|
||||
const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
|
||||
this.password = await bcrypt.hash(this.password, salt); // hash is also async, need to eitehr await or use hashSync
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
this.password = bcrypt.hashSync(this.password, salt);
|
||||
next();
|
||||
});
|
||||
|
||||
UserSchema.pre("findOneAndUpdate", async function (next) {
|
||||
UserSchema.pre("findOneAndUpdate", function (next) {
|
||||
const update = this.getUpdate();
|
||||
if ("password" in update) {
|
||||
const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait
|
||||
update.password = await bcrypt.hash(update.password, salt); // hash is also async, need to eitehr await or use hashSync
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
update.password = bcrypt.hashSync(update.password, salt);
|
||||
}
|
||||
|
||||
next();
|
||||
@@ -89,4 +89,4 @@ User.init().then(() => {
|
||||
});
|
||||
});
|
||||
|
||||
export default mongoose.model("User", UserSchema);
|
||||
export default User;
|
||||
|
||||
@@ -96,6 +96,7 @@ class MongoDB {
|
||||
try {
|
||||
const connectionString =
|
||||
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
|
||||
console.log("Connecting to MongoDB with connection string:", connectionString);
|
||||
await mongoose.connect(connectionString);
|
||||
// If there are no AppSettings, create one
|
||||
await AppSettings.findOneAndUpdate(
|
||||
|
||||
@@ -751,7 +751,7 @@ const createMonitor = async (req, res) => {
|
||||
*/
|
||||
const createBulkMonitors = async (req) => {
|
||||
try {
|
||||
const monitors = req.body.map(
|
||||
const monitors = req.map(
|
||||
(item) => new Monitor({ ...item, notifications: undefined })
|
||||
);
|
||||
await Monitor.bulkSave(monitors);
|
||||
|
||||
@@ -38,6 +38,8 @@ const updateStatusPage = async (statusPageData, image) => {
|
||||
data: image.buffer,
|
||||
contentType: image.mimetype,
|
||||
};
|
||||
}else{
|
||||
statusPageData.logo = null;
|
||||
}
|
||||
|
||||
if (statusPageData.deleteSubmonitors === "true") {
|
||||
|
||||
@@ -81,7 +81,9 @@ import ServiceRegistry from "./service/serviceRegistry.js";
|
||||
|
||||
import MongoDB from "./db/mongo/MongoDB.js";
|
||||
|
||||
// Redis Service and dependencies
|
||||
import IORedis from "ioredis";
|
||||
import RedisService from "./service/redisService.js";
|
||||
|
||||
import TranslationService from "./service/translationService.js";
|
||||
import languageMiddleware from "./middleware/languageMiddleware.js";
|
||||
@@ -113,21 +115,16 @@ const shutdown = async () => {
|
||||
method: "shutdown",
|
||||
});
|
||||
// flush Redis
|
||||
const settings =
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings() || {};
|
||||
const redisService = ServiceRegistry.get(RedisService.SERVICE_NAME);
|
||||
await redisService.flushall();
|
||||
|
||||
const { redisUrl } = settings;
|
||||
const redis = new IORedis(redisUrl, { maxRetriesPerRequest: null });
|
||||
|
||||
logger.info({ message: "Flushing Redis" });
|
||||
await redis.flushall();
|
||||
logger.info({ message: "Redis flushed" });
|
||||
process.exit(1);
|
||||
}, SHUTDOWN_TIMEOUT);
|
||||
try {
|
||||
server.close();
|
||||
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
|
||||
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
|
||||
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushall();
|
||||
logger.info({ message: "Graceful shutdown complete" });
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
@@ -185,7 +182,13 @@ const startApp = async () => {
|
||||
stringService
|
||||
);
|
||||
|
||||
const jobQueue = new JobQueue(
|
||||
const redisService = await RedisService.createInstance({
|
||||
logger,
|
||||
IORedis,
|
||||
SettingsService: settingsService,
|
||||
});
|
||||
|
||||
const jobQueue = new JobQueue({
|
||||
db,
|
||||
statusService,
|
||||
networkService,
|
||||
@@ -194,8 +197,9 @@ const startApp = async () => {
|
||||
stringService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
);
|
||||
Worker,
|
||||
redisService,
|
||||
});
|
||||
|
||||
// Register services
|
||||
ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
|
||||
@@ -207,6 +211,7 @@ const startApp = async () => {
|
||||
ServiceRegistry.register(StatusService.SERVICE_NAME, statusService);
|
||||
ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService);
|
||||
ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService);
|
||||
ServiceRegistry.register(RedisService.SERVICE_NAME, redisService);
|
||||
|
||||
await translationService.initialize();
|
||||
|
||||
@@ -231,7 +236,8 @@ const startApp = async () => {
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
ServiceRegistry.get(JobQueue.SERVICE_NAME),
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME)
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME),
|
||||
ServiceRegistry.get(EmailService.SERVICE_NAME),
|
||||
);
|
||||
|
||||
const settingsController = new SettingsController(
|
||||
@@ -343,7 +349,7 @@ const startApp = async () => {
|
||||
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
||||
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
|
||||
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
|
||||
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
|
||||
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
|
||||
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
|
||||
app.use("/api/v1/health", (req, res) => {
|
||||
res.json({
|
||||
|
||||
@@ -158,5 +158,8 @@
|
||||
"platformRequired": "Platform is required",
|
||||
"testNotificationFailed": "Failed to send test notification",
|
||||
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\n📟 Status Code: {code}\n\u200B\n",
|
||||
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n"
|
||||
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
|
||||
"sendTestEmail": "Test email sent successfully",
|
||||
"errorForValidEmailAddress": "A valid recipient email address is required.",
|
||||
"testEmailSubject": "Test Email from Monitoring System"
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const isAllowed = (allowedRoles) => {
|
||||
return;
|
||||
} else {
|
||||
const error = new Error(stringService.insufficientPermissions);
|
||||
error.status = 401;
|
||||
error.status = 403;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import logger from "../utils/logger.js";
|
||||
|
||||
const languageMiddleware =
|
||||
(stringService, translationService, settingsService) => async (req, res, next) => {
|
||||
(stringService, translationService) => async (req, res, next) => {
|
||||
try {
|
||||
const settings = await settingsService.getSettings();
|
||||
|
||||
let language = settings && settings.language ? settings.language : null;
|
||||
|
||||
if (!language) {
|
||||
const acceptLanguage = req.headers["accept-language"] || "en";
|
||||
language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
|
||||
}
|
||||
const acceptLanguage = req.headers["accept-language"] || "en";
|
||||
const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
|
||||
|
||||
translationService.setLanguage(language);
|
||||
stringService.setLanguage(language);
|
||||
|
||||
468
server/package-lock.json
generated
468
server/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bcryptjs": "3.0.2",
|
||||
"bullmq": "5.41.2",
|
||||
"compression": "1.8.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -850,26 +850,6 @@
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^5.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
|
||||
@@ -1166,12 +1146,6 @@
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -1289,26 +1263,6 @@
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -1389,20 +1343,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"node-addon-api": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
@@ -1412,6 +1352,15 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -1852,15 +1801,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -1983,15 +1923,6 @@
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/colord": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
@@ -2112,6 +2043,7 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
@@ -2159,12 +2091,6 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -2522,12 +2448,6 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@@ -3439,36 +3359,6 @@
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -3492,74 +3382,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.2",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gauge/node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/gauge/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
|
||||
@@ -3708,12 +3530,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -3920,17 +3736,6 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -4572,30 +4377,6 @@
|
||||
"qs": "^6.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -4716,31 +4497,6 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mjml": {
|
||||
"version": "5.0.0-alpha.6",
|
||||
"resolved": "https://registry.npmjs.org/mjml/-/mjml-5.0.0-alpha.6.tgz",
|
||||
@@ -5459,12 +5215,6 @@
|
||||
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -5613,21 +5363,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -5637,19 +5372,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^2.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^3.0.0",
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@@ -5863,15 +5585,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -6743,65 +6456,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -6922,12 +6576,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -7443,23 +7091,6 @@
|
||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
||||
@@ -7494,27 +7125,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
|
||||
@@ -8004,56 +7614,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wide-align/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
@@ -8224,12 +7784,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bcryptjs": "3.0.2",
|
||||
"bullmq": "5.41.2",
|
||||
"compression": "1.8.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -33,8 +33,9 @@
|
||||
"mailersend": "^2.2.0",
|
||||
"mjml": "^5.0.0-alpha.4",
|
||||
"mongoose": "^8.3.3",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
"papaparse": "^5.5.2",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import multer from "multer";
|
||||
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage() // Store file in memory as Buffer
|
||||
});
|
||||
|
||||
class MonitorRoutes {
|
||||
constructor(monitorController) {
|
||||
this.router = Router();
|
||||
@@ -89,10 +94,17 @@ class MonitorRoutes {
|
||||
this.router.post(
|
||||
"/bulk",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
upload.single("csvFile"),
|
||||
this.monitorController.createBulkMonitors
|
||||
);
|
||||
|
||||
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
|
||||
|
||||
this.router.post(
|
||||
"/test-email",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
this.monitorController.sendTestEmail
|
||||
);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -12,7 +12,7 @@ class SettingsRoutes {
|
||||
this.router.get("/", this.settingsController.getAppSettings);
|
||||
this.router.put(
|
||||
"/",
|
||||
isAllowed(["superadmin"]),
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
this.settingsController.updateAppSettings
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class EmailService {
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
|
||||
testEmailTemplate: this.loadTemplate("testEmailTemplate")
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import IORedis from "ioredis";
|
||||
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const JOBS_PER_WORKER = 5;
|
||||
@@ -18,7 +16,7 @@ const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
|
||||
class NewJobQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor(
|
||||
constructor({
|
||||
db,
|
||||
statusService,
|
||||
networkService,
|
||||
@@ -27,16 +25,14 @@ class NewJobQueue {
|
||||
stringService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
) {
|
||||
const settings = settingsService.getSettings() || {};
|
||||
const { redisUrl } = settings;
|
||||
const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
|
||||
Worker,
|
||||
redisService,
|
||||
}) {
|
||||
this.connection = redisService.getConnection();
|
||||
this.queues = {};
|
||||
this.workers = {};
|
||||
this.lastJobProcessedTime = {};
|
||||
|
||||
this.connection = connection;
|
||||
this.db = db;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
@@ -47,7 +43,7 @@ class NewJobQueue {
|
||||
this.stringService = stringService;
|
||||
|
||||
QUEUE_NAMES.forEach((name) => {
|
||||
const q = new Queue(name, { connection });
|
||||
const q = new Queue(name, { connection: this.connection });
|
||||
this.lastJobProcessedTime[q.name] = Date.now();
|
||||
q.on("error", (error) => {
|
||||
this.logger.error({
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import jmespath from "jmespath";
|
||||
import https from "https";
|
||||
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
|
||||
|
||||
@@ -133,6 +135,7 @@ class NetworkService {
|
||||
name,
|
||||
teamId,
|
||||
type,
|
||||
ignoreTlsErrors,
|
||||
jsonPath,
|
||||
matchMethod,
|
||||
expectedValue,
|
||||
@@ -141,6 +144,12 @@ class NetworkService {
|
||||
|
||||
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
|
||||
|
||||
if (ignoreTlsErrors === true) {
|
||||
config.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
}
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.axios.get(url, config)
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user