Compare commits

...

53 commits

Author SHA1 Message Date
Dj Isaac
64f528456d
Merge branch 'master' into dj/discord-service-url 2024-10-20 19:29:05 -05:00
Dj Isaac
cd69146f33
add discord buttons to payload 2024-10-20 19:28:47 -05:00
Suven-p
c01494ec33
feat: add RabbitMQ monitor (#5199)
Some checks failed
Auto Test / auto-test (18, ARM64) (push) Has been cancelled
Auto Test / auto-test (18, macos-latest) (push) Has been cancelled
Auto Test / auto-test (18, ubuntu-latest) (push) Has been cancelled
Auto Test / auto-test (18, windows-latest) (push) Has been cancelled
Auto Test / auto-test (20, ARM64) (push) Has been cancelled
Auto Test / auto-test (20, macos-latest) (push) Has been cancelled
Auto Test / auto-test (20, ubuntu-latest) (push) Has been cancelled
Auto Test / auto-test (20, windows-latest) (push) Has been cancelled
Auto Test / armv7-simple-test (18, ARMv7) (push) Has been cancelled
Auto Test / armv7-simple-test (20, ARMv7) (push) Has been cancelled
Auto Test / check-linters (push) Has been cancelled
Auto Test / e2e-test (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
json-yaml-validate / json-yaml-validate (push) Has been cancelled
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-20 15:57:03 +02:00
Ryo Hanafusa
a7e9bdd43e
fix: expand hover trigger area of beat (#5223)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-20 15:30:03 +02:00
sctnightcore
d7ffa33950
feat: add notification provider SendGrid (#5205)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-18 02:25:23 +02:00
Frank Elsinga
a20a43b8aa
tests: add testcase for the mqtt monitor with testcontainers (#4451) 2024-10-17 16:25:45 +02:00
Frank Elsinga
ed6087e233
fix: slack rich notification not working correctly (#5209) 2024-10-16 16:50:41 +02:00
Frank Elsinga
42e77798e5
Revert "Extend length of status bars in status page" (#5207) 2024-10-16 14:58:44 +02:00
Peace
85dfe1f5d1
fix: some errors from the performance optimization (#5196) 2024-10-16 13:25:35 +02:00
Louis Lam
b719d11500 Merge branch 'master' into fix-weblate-conflict2
# Conflicts:
#	src/lang/bg-BG.json
#	src/lang/cs-CZ.json
#	src/lang/de-CH.json
#	src/lang/de-DE.json
#	src/lang/fr-FR.json
#	src/lang/ga.json
#	src/lang/id-ID.json
#	src/lang/tr-TR.json
#	src/lang/uk-UA.json
#	src/lang/zh-CN.json
2024-10-15 00:56:41 +08:00
Ömer Faruk Genç
b7d2cedf2e Translated using Weblate (Turkish)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
2024-10-14 15:53:13 +00:00
Buchtič
f3ee9c2cad Translated using Weblate (Czech)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
2024-10-14 15:53:13 +00:00
Mohit Nagaraj
582fb6c5ad Added translation using Weblate (English (Middle)) 2024-10-12 03:23:55 +00:00
Erlaan
dda40610c7
feat: new notification provider 46elks (#5184)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-11 11:07:34 +02:00
Arjun Komath
bafca6bd37
feat: support sound and title in TechulusPush (#5178) 2024-10-11 00:19:58 +02:00
Jason Michael
bbc75b840b
docs: grammatical improvements in contribution guide (#5183)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-10 08:57:59 +02:00
YangHongYu
365aa8d814 Added translation using Weblate (Abkhazian) 2024-10-09 13:42:02 +00:00
YangHongYu
7e37dacb9a Added translation using Weblate (Uyghur) 2024-10-09 13:40:32 +00:00
AnnAngela
958f96f06d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
2024-10-07 08:42:10 +00:00
Txopi
130d8d0177 Translated using Weblate (Basque)
Currently translated at 54.8% (557 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/eu/
2024-10-06 00:27:20 +00:00
Aindriú Mac Giolla Eoin
9905ca574c Translated using Weblate (Irish)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ga/
2024-10-05 14:27:30 +00:00
janinainfa
fc429108ac Translated using Weblate (Polish)
Currently translated at 94.2% (958 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/
2024-10-05 14:27:30 +00:00
Cyril59310
10ffde2595 Translated using Weblate (French)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
2024-10-05 14:27:30 +00:00
stanol
b9b48e1b2d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
2024-10-04 11:50:37 +00:00
Harry Suryapambagya
a760898281 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
2024-10-04 11:50:37 +00:00
Marco
289b72d07d Translated using Weblate (German)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
2024-10-04 11:50:37 +00:00
Marco
9257a7a19e Translated using Weblate (German (Switzerland))
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
2024-10-04 11:50:37 +00:00
MrEddX
8084c7e61c Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
2024-10-04 11:50:31 +00:00
Aung ko
324d879aad Translated using Weblate (Burmese)
Currently translated at 7.3% (75 of 1016 strings)

Translated using Weblate (English)

Currently translated at 99.9% (1015 of 1016 strings)

Co-authored-by: Aung ko <aungko.use@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/en/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/my/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Satveek Gupta
d2a44487b3 Translated using Weblate (Hindi)
Currently translated at 5.9% (60 of 1016 strings)

Co-authored-by: Satveek Gupta <guptasatveek@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ömer Faruk GÖL
35668219ec Translated using Weblate (Turkish)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Ömer Faruk GÖL <omerfarukgol@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Lucius Gaitán
895f6d2ff1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.2% (978 of 1016 strings)

Co-authored-by: Lucius Gaitán <lucius@luciusgaitan.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Shinwoo PARK
3088cc6141 Translated using Weblate (Korean)
Currently translated at 73.3% (745 of 1016 strings)

Co-authored-by: Shinwoo PARK <devpysweb@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Mohammad Hesam Imani
98415bd419 Translated using Weblate (Persian)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Mohammad Hesam Imani <hesamimany@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
AlanFarley
087d20b775 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.9% (965 of 1016 strings)

Co-authored-by: AlanFarley <alanfarley2010@live.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ivan M
ec7923f4fd Translated using Weblate (Belarusian)
Currently translated at 91.8% (933 of 1016 strings)

Co-authored-by: Ivan M <ivan.meishutovich@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/be/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
B3CKDOOR
86b3ff6bfd Translated using Weblate (Dutch)
Currently translated at 99.0% (1006 of 1016 strings)

Co-authored-by: B3CKDOOR <B3CKDOOR@users.noreply.weblate.kuma.pet>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/nl/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Elia Ronchetti
e6159d9ab4 Translated using Weblate (Italian)
Currently translated at 69.3% (705 of 1016 strings)

Co-authored-by: Elia Ronchetti <e.ronchetti@campus.unimib.it>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ossie Stinga
e781325633 Translated using Weblate (Italian)
Currently translated at 69.3% (705 of 1016 strings)

Co-authored-by: Ossie Stinga <ossie.stinga@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Sajjad Jazini
72478090e7 Translated using Weblate (Persian)
Currently translated at 98.1% (997 of 1016 strings)

Co-authored-by: Sajjad Jazini <s.jazini@vatanmail.ir>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
MrEddX
50ec9fec05 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Aindriú Mac Giolla Eoin
62c55f0e25 Translated using Weblate (Irish)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ga/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Hyeonho Kang
3ab35c38fc Translated using Weblate (Korean)
Currently translated at 72.9% (741 of 1016 strings)

Co-authored-by: Hyeonho Kang <hyeonhokang10@o365.ice.go.kr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
stanol
1d86fa2b5c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
kindercat
cef072cae9 Translated using Weblate (Romanian)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (Romanian)

Currently translated at 95.5% (971 of 1016 strings)

Co-authored-by: kindercat <156439718+kindercat@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ro/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
AnnAngela
abcc98c836 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Lee Min Waan
aa38344c3d Translated using Weblate (Vietnamese)
Currently translated at 51.8% (527 of 1016 strings)

Co-authored-by: Lee Min Waan <rakis9076@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/vi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Harry Suryapambagya
9ff0ae67df Translated using Weblate (Indonesian)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.1% (967 of 1016 strings)

Co-authored-by: Harry Suryapambagya <harsxv@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Michal
a14d05daab Translated using Weblate (Czech)
Currently translated at 96.1% (977 of 1016 strings)

Co-authored-by: Michal <black23@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ilkka Myller
93c5ab0bd8 Translated using Weblate (Finnish)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Marco
d27a9e7d7f Translated using Weblate (German)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Cyril59310
c120c37030 Translated using Weblate (French)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (French)

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Louis Lam
acd1e7211a Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 70.9% (721 of 1016 strings)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
40 changed files with 1777 additions and 175 deletions

View file

@ -1,6 +1,6 @@
# Project Info
First of all, I want to thank everyone who have wrote issues or shared pull requests for Uptime Kuma.
First of all, I want to thank everyone who has submitted issues or shared pull requests for Uptime Kuma.
I never thought the GitHub community would be so nice!
Because of this, I also never thought that other people would actually read and edit my code.
Parts of the code are not very well-structured or commented, sorry about that.
@ -9,7 +9,7 @@ The project was created with `vite.js` and is written in `vue3`.
Our backend lives in the `server`-directory and mostly communicates via websockets.
Both frontend and backend share the same `package.json`.
For production, the frontend is build into `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For production, the frontend is built into the `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For development, we run vite in development mode on another port.
## Directories
@ -28,7 +28,7 @@ For development, we run vite in development mode on another port.
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do.
Both your and our maintainers time is precious, and we don't want to waste both time.
Both yours and our maintainers' time is precious, and we don't want to waste either.
If you have any questions about any process/.. is not clear, you are likely not alone => please ask them ^^
@ -49,11 +49,11 @@ Different guidelines exist for different types of pull requests (PRs):
<p>
If you come across a bug and think you can solve, we appreciate your work.
Please make sure that you follow by these rules:
Please make sure that you follow these rules:
- keep the PR as small as possible, fix only one thing at a time => keeping it reviewable
- test that your code does what you came it does.
- test that your code does what you claim it does.
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>translations / internationalisation (i18n)</b></summary>
@ -68,7 +68,7 @@ Different guidelines exist for different types of pull requests (PRs):
- language keys need to be **added to `en.json`** to be visible in weblate. If this has not happened, a PR is appreciated.
- **Adding a new language** requires a new file see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md)
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new notification providers</b></summary>
@ -102,7 +102,7 @@ Different guidelines exist for different types of pull requests (PRs):
Therefore, making sure that they work is also really important.
Because testing notification providers is quite time intensive, we mostly offload this onto the person contributing a notification provider.
To make shure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
To make sure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
- `UP`/`DOWN`
- Certificate Expiry via https://expired.badssl.com/
- Testing (the test button on the notification provider setup page)
@ -117,7 +117,7 @@ Different guidelines exist for different types of pull requests (PRs):
| Testing | paste-image-here | paste-image-here |
```
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new monitoring types</b></summary>
@ -138,14 +138,14 @@ Different guidelines exist for different types of pull requests (PRs):
-
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new features/ major changes / breaking bugfixes</b></summary>
<p>
be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**.
This is especially important for a large pull request or you don't know if it will be merged or not.
This is especially important for a large pull request or when you don't know if it will be merged or not.
<sub>Because of the large impact of this work, only senior maintainers may merge PRs in this area.</sub>
</p>
@ -201,7 +201,7 @@ The rationale behind this is that we can align the direction and scope of the fe
## Project Styles
I personally do not like something that requires so many configurations before you can finally start the app.
I personally do not like something that requires a lot of configuration before you can finally start the app.
The goal is to make the Uptime Kuma installation as easy as installing a mobile app.
- Easy to install for non-Docker users
@ -260,7 +260,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
But sometimes you may want to restart the server without restarting the frontend. In that case, you can run these commands in two terminals:
```bash
npm run start-frontend-dev
@ -409,7 +409,7 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### What is a maintainer and what are their roles?
This project has multiple maintainers which specialise in different areas.
This project has multiple maintainers who specialise in different areas.
Currently, there are 3 maintainers:
| Person | Role | Main Area |

View file

@ -0,0 +1,17 @@
exports.up = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.text("rabbitmq_nodes");
table.string("rabbitmq_username");
table.string("rabbitmq_password");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("rabbitmq_nodes");
table.dropColumn("rabbitmq_username");
table.dropColumn("rabbitmq_password");
});
};

997
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -154,6 +154,8 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1",
"@testcontainers/rabbitmq": "^10.13.2",
"@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
@ -189,6 +191,7 @@
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"test": "~3.3.0",
"testcontainers": "^10.13.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.2.8",

View file

@ -153,6 +153,7 @@ class Monitor extends BeanModel {
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions),
};
@ -183,6 +184,8 @@ class Monitor extends BeanModel {
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
rabbitmqUsername: this.rabbitmqUsername,
rabbitmqPassword: this.rabbitmqPassword,
};
}
@ -1508,10 +1511,8 @@ class Monitor extends BeanModel {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (?)
`, [
monitorIDs,
]);
WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}
/**
@ -1521,13 +1522,11 @@ class Monitor extends BeanModel {
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, tag.name, tag.color
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (?)
`, [
monitorIDs,
]);
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}
/**
@ -1567,6 +1566,7 @@ class Monitor extends BeanModel {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
tag_id: row.tag_id,
name: row.name,
color: row.color
});

View file

@ -0,0 +1,67 @@
const { MonitorType } = require("./monitor-type");
const { log, UP, DOWN } = require("../../src/util");
const { axiosAbortSignal } = require("../util-server");
const axios = require("axios");
class RabbitMqMonitorType extends MonitorType {
name = "rabbitmq";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
let baseUrls = [];
try {
baseUrls = JSON.parse(monitor.rabbitmqNodes);
} catch (error) {
throw new Error("Invalid RabbitMQ Nodes");
}
heartbeat.status = DOWN;
for (let baseUrl of baseUrls) {
try {
// Without a trailing slash, path in baseUrl will be removed. https://example.com/api -> https://example.com
if ( !baseUrl.endsWith("/") ) {
baseUrl += "/";
}
const options = {
// Do not start with slash, it will strip the trailing slash from baseUrl
url: new URL("api/health/checks/alarms/", baseUrl).href,
method: "get",
timeout: monitor.timeout * 1000,
headers: {
"Accept": "application/json",
"Authorization": "Basic " + Buffer.from(`${monitor.rabbitmqUsername || ""}:${monitor.rabbitmqPassword || ""}`).toString("base64"),
},
signal: axiosAbortSignal((monitor.timeout + 10) * 1000),
// Capture reason for 503 status
validateStatus: (status) => status === 200 || status === 503,
};
log.debug("monitor", `[${monitor.name}] Axios Request: ${JSON.stringify(options)}`);
const res = await axios.request(options);
log.debug("monitor", `[${monitor.name}] Axios Response: status=${res.status} body=${JSON.stringify(res.data)}`);
if (res.status === 200) {
heartbeat.status = UP;
heartbeat.msg = "OK";
break;
} else if (res.status === 503) {
heartbeat.msg = res.data.reason;
} else {
heartbeat.msg = `${res.status} - ${res.statusText}`;
}
} catch (error) {
if (axios.isCancel(error)) {
heartbeat.msg = "Request timed out";
log.debug("monitor", `[${monitor.name}] Request timed out`);
} else {
log.debug("monitor", `[${monitor.name}] Axios Error: ${JSON.stringify(error.message)}`);
heartbeat.msg = error.message;
}
}
}
}
}
module.exports = {
RabbitMqMonitorType,
};

View file

@ -0,0 +1,35 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Elks extends NotificationProvider {
name = "Elks";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.46elks.com/a1/sms";
try {
let data = new URLSearchParams();
data.append("from", notification.elksFromNumber);
data.append("to", notification.elksToNumber );
data.append("message", msg);
const config = {
headers: {
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
}
};
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Elks;

View file

@ -1,6 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
const { Settings } = require("../settings");
const { DOWN, UP, getMonitorRelativeURL } = require("../../src/util");
class Discord extends NotificationProvider {
name = "discord";
@ -19,8 +20,13 @@ class Discord extends NotificationProvider {
}
// If heartbeatJSON is null, assume we're testing.
const baseURL = await Settings.get("primaryBaseURL");
const address = this.extractAddress(monitorJSON);
const hasAddress = address !== "" && address !== monitorJSON.hostname;
if (heartbeatJSON == null) {
let discordtestdata = {
const discordtestdata = {
username: discordDisplayName,
content: msg,
};
@ -33,20 +39,18 @@ class Discord extends NotificationProvider {
return okMsg;
}
const address = this.extractAddress(monitorJSON);
const embedFields = [
{
name: "Service Name",
value: monitorJSON["name"],
value: monitorJSON.name,
},
...((address !== "" && address !== monitorJSON["hostname"]) ? [{
...(hasAddress ? [{
name: "Service URL",
value: address
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
name: `Time (${heartbeatJSON.timezone})`,
value: heartbeatJSON.localDateTime,
},
{
name: "Error",
@ -54,50 +58,70 @@ class Discord extends NotificationProvider {
},
];
const components = [
{
type: 1, // Action Row
components: [
baseURL && {
type: 2, // Button
style: 5, // Link Button,
label: "Visit Uptime Kuma",
url: baseURL + getMonitorRelativeURL(monitorJSON.id)
},
hasAddress && {
type: 2, // Button
style: 5, // Link Button,
label: "Visit Service URL",
url: address
}
].filter(Boolean) // remove invalid data
}
];
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] === DOWN) {
let discorddowndata = {
if (heartbeatJSON.status === DOWN) {
const discorddowndata = {
username: discordDisplayName,
content: notification.discordPrefixMessage || "",
embeds: [{
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
title: `❌ Your service ${monitorJSON.name} went down. ❌`,
color: 16711680,
timestamp: heartbeatJSON["time"],
timestamp: heartbeatJSON.time,
fields: embedFields,
}],
components: components,
};
if (notification.discordChannelType === "createNewForumPost") {
discorddowndata.thread_name = notification.postName;
}
if (notification.discordPrefixMessage) {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(webhookUrl.toString(), discorddowndata);
return okMsg;
}
} else if (heartbeatJSON["status"] === UP) {
let discordupdata = {
if (heartbeatJSON.status === UP) {
const discordupdata = {
username: discordDisplayName,
content: notification.discordPrefixMessage || "",
embeds: [{
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
title: `✅ Your service ${monitorJSON.name} is up! ✅`,
color: 65280,
timestamp: heartbeatJSON["time"],
timestamp: heartbeatJSON.time,
fields: embedFields,
}],
components: components,
};
if (notification.discordChannelType === "createNewForumPost") {
discordupdata.thread_name = notification.postName;
}
if (notification.discordPrefixMessage) {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(webhookUrl.toString(), discordupdata);
return okMsg;
}
} catch (error) {
console.log(error);
this.throwGeneralAxiosError(error);
}
}

View file

@ -0,0 +1,65 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SendGrid extends NotificationProvider {
name = "SendGrid";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${notification.sendgridApiKey}`,
},
};
let personalizations = {
to: [{ email: notification.sendgridToEmail }],
};
// Add CC recipients if provided
if (notification.sendgridCcEmail) {
personalizations.cc = notification.sendgridCcEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
// Add BCC recipients if provided
if (notification.sendgridBccEmail) {
personalizations.bcc = notification.sendgridBccEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
let data = {
personalizations: [ personalizations ],
from: { email: notification.sendgridFromEmail.trim() },
subject:
notification.sendgridSubject ||
"Notification from Your Uptime Kuma",
content: [
{
type: "text/plain",
value: msg,
},
],
};
await axios.post(
"https://api.sendgrid.com/v3/mail/send",
data,
config
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SendGrid;

View file

@ -32,7 +32,7 @@ class Slack extends NotificationProvider {
* @param {object} monitorJSON The monitor config
* @returns {Array} The relevant action objects
*/
static buildActions(baseURL, monitorJSON) {
buildActions(baseURL, monitorJSON) {
const actions = [];
if (baseURL) {
@ -73,7 +73,7 @@ class Slack extends NotificationProvider {
* @param {string} msg The message body
* @returns {Array<object>} The rich content blocks for the Slack message
*/
static buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
//create an array to dynamically add blocks
const blocks = [];
@ -150,7 +150,7 @@ class Slack extends NotificationProvider {
data.attachments.push(
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
"blocks": this.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
);
} else {

View file

@ -10,11 +10,22 @@ class TechulusPush extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
let data = {
"title": notification?.pushTitle?.length ? notification.pushTitle : "Uptime-Kuma",
"body": msg,
"timeSensitive": notification.pushTimeSensitive ?? true,
};
if (notification.pushChannel) {
data.channel = notification.pushChannel;
}
if (notification.pushSound) {
data.sound = notification.pushSound;
}
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
});
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View file

@ -11,6 +11,7 @@ const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
@ -67,6 +68,7 @@ const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
class Notification {
@ -95,6 +97,7 @@ class Notification {
new SMSC(),
new DingDing(),
new Discord(),
new Elks(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),
@ -151,6 +154,7 @@ class Notification {
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
new SendGrid()
];
for (let item of list) {
if (! item.name) {

View file

@ -718,6 +718,8 @@ let needSetup = false;
monitor.conditions = JSON.stringify(monitor.conditions);
monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
bean.import(monitor);
bean.user_id = socket.userID;
@ -868,6 +870,9 @@ let needSetup = false;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout;
bean.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
bean.rabbitmqUsername = monitor.rabbitmqUsername;
bean.rabbitmqPassword = monitor.rabbitmqPassword;
bean.conditions = JSON.stringify(monitor.conditions);
bean.validate();

View file

@ -115,6 +115,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@ -552,4 +553,5 @@ const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const Monitor = require("./model/monitor");

View file

@ -4,11 +4,17 @@
<div
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
class="beat-hover-area"
:class="{ 'empty': (beat === 0) }"
:style="beatHoverAreaStyle"
:title="getBeatTitle(beat)"
/>
>
<div
class="beat"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
/>
</div>
</div>
<div
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
@ -47,7 +53,7 @@ export default {
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
beatMargin: 4,
beatHoverAreaPadding: 4,
move: false,
maxBeat: -1,
};
@ -123,7 +129,7 @@ export default {
barStyle() {
if (this.move && this.shortBeatList.length > this.maxBeat) {
let width = -(this.beatWidth + this.beatMargin * 2);
let width = -(this.beatWidth + this.beatHoverAreaPadding * 2);
return {
transition: "all ease-in-out 0.25s",
@ -137,12 +143,17 @@ export default {
},
beatHoverAreaStyle() {
return {
padding: this.beatHoverAreaPadding + "px",
"--hover-scale": this.hoverScale,
};
},
beatStyle() {
return {
width: this.beatWidth + "px",
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
};
},
@ -152,7 +163,7 @@ export default {
*/
timeStyle() {
return {
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px",
"margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px",
};
},
@ -219,20 +230,20 @@ export default {
if (this.size !== "big") {
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
this.beatHoverAreaPadding = 2;
}
// Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
const actualHoverAreaPadding = this.beatHoverAreaPadding * window.devicePixelRatio;
if (!Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (!Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
if (!Number.isInteger(actualHoverAreaPadding)) {
this.beatHoverAreaPadding = Math.round(actualHoverAreaPadding) / window.devicePixelRatio;
}
window.addEventListener("resize", this.resize);
@ -245,7 +256,7 @@ export default {
*/
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
}
},
@ -273,32 +284,41 @@ export default {
}
.hp-bar-big {
.beat {
.beat-hover-area {
display: inline-block;
background-color: $primary;
border-radius: $border-radius;
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
&:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8;
transform: scale(var(--hover-scale));
}
.beat {
background-color: $primary;
border-radius: $border-radius;
/*
pointer-events needs to be changed because
tooltip momentarily disappears when crossing between .beat-hover-area and .beat
*/
pointer-events: none;
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
}
}
}

View file

@ -118,6 +118,7 @@ export default {
"clicksendsms": "ClickSend SMS",
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
"discord": "Discord",
"Elks": "46elks",
"GoogleChat": "Google Chat (Google Workspace)",
"gorush": "Gorush",
"gotify": "Gotify",
@ -164,6 +165,7 @@ export default {
"whapi": "WhatsApp (Whapi)",
"gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt",
"SendGrid": "SendGrid"
};
// Put notifications here if it's not supported in most regions or its documentation is not in English

View file

@ -33,7 +33,7 @@
<template #item="monitor">
<div class="item" data-testid="monitor">
<div class="row">
<div class="col-6 col-md-4 small-padding">
<div class="col-9 col-md-8 small-padding">
<div class="info">
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
@ -71,7 +71,7 @@
</div>
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
</div>
</div>

View file

@ -0,0 +1,48 @@
<template>
<div class="mb-3">
<label for="ElksUsername" class="form-label">{{ $t("Username") }}</label>
<input id="ElksUsername" v-model="$parent.notification.elksUsername" type="text" class="form-control" required>
<label for="ElksPassword" class="form-label">{{ $t("Password") }}</label>
</div>
<div class="form-text">
<HiddenInput id="ElksPassword" v-model="$parent.notification.elksAuthToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="p" keypath="Can be found on:">
<a href="https://46elks.com/account" target="_blank">https://46elks.com/account</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="Elks-from-number" class="form-label">{{ $t("From") }}</label>
<input id="Elks-from-number" v-model="$parent.notification.elksFromNumber" type="text" class="form-control" required>
<div class="form-text">
{{ $t("Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.") }}
<i18n-t tag="p" keypath="More info on:">
<a href="https://46elks.se/kb/text-sender-id" target="_blank">https://46elks.se/kb/text-sender-id</a>
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="Elks-to-number" class="form-label">{{ $t("To Number") }}</label>
<input id="Elks-to-number" v-model="$parent.notification.elksToNumber" type="text" class="form-control" required>
<div class="form-text">
{{ $t("The phone number of the recipient in E.164 format.") }}
<i18n-t tag="p" keypath="More info on:">
<a href="https://46elks.se/kb/e164" target="_blank">https://46elks.se/kb/e164</a>
</i18n-t>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://46elks.com/docs/send-sms" target="_blank">https://46elks.com/docs/send-sms</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -4,6 +4,12 @@
<input id="discord-webhook-url" v-model="$parent.notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
<div class="form-text">
{{ $t("wayToGetDiscordURL") }}
<i18n-t keypath="wayToGetDiscordURLEnhanced">
<a
href="https://discord.com/developers/docs/resources/webhook#create-webhook"
target="_blank"
>{{ $t("withADiscordBot") }}</a>
</i18n-t>
</div>
</div>

View file

@ -0,0 +1,47 @@
<template>
<div class="mb-3">
<label for="sendgrid-api-key" class="form-label">{{ $t("SendGrid API Key") }}</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.sendgridApiKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="sendgrid-from-email" class="form-label">{{ $t("From Email") }}</label>
<input id="sendgrid-from-email" v-model="$parent.notification.sendgridFromEmail" type="email" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-to-email" class="form-label">{{ $t("To Email") }}</label>
<input id="sendgrid-to-email" v-model="$parent.notification.sendgridToEmail" type="email" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-cc-email" class="form-label">{{ $t("smtpCC") }}</label>
<input id="sendgrid-cc-email" v-model="$parent.notification.sendgridCcEmail" type="email" class="form-control">
<div class="form-text">{{ $t("Separate multiple email addresses with commas") }}</div>
</div>
<div class="mb-3">
<label for="sendgrid-bcc-email" class="form-label">{{ $t("smtpBCC") }}</label>
<input id="sendgrid-bcc-email" v-model="$parent.notification.sendgridBccEmail" type="email" class="form-control">
<small class="form-text text-muted">{{ $t("Separate multiple email addresses with commas") }}</small>
</div>
<div class="mb-3">
<label for="sendgrid-subject" class="form-label">{{ $t("Subject:") }}</label>
<input id="sendgrid-subject" v-model="$parent.notification.sendgridSubject" type="text" class="form-control">
<small class="form-text text-muted">{{ $t("leave blank for default subject") }}</small>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.sendgrid.com/api-reference/mail-send/mail-send" target="_blank">https://docs.sendgrid.com/api-reference/mail-send/mail-send</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.sendgridSubject === "undefined") {
this.$parent.notification.sendgridSubject = "Notification from Your Uptime Kuma";
}
},
};
</script>

View file

@ -4,6 +4,53 @@
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="push-api-title" class="form-label">{{ $t("Title") }}</label>
<input id="push-api-title" v-model="$parent.notification.pushTitle" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="push-api-channel" class="form-label">{{ $t("Notification Channel") }}</label>
<input id="push-api-channel" v-model="$parent.notification.pushChannel" type="text" class="form-control" patttern="[A-Za-z0-9-]+">
<div class="form-text">
{{ $t("Alphanumerical string and hyphens only") }}
</div>
</div>
<div class="mb-3">
<label for="push-api-sound" class="form-label">{{ $t("Sound") }}</label>
<select id="push-api-sound" v-model="$parent.notification.pushSound" class="form-select">
<option value="default">{{ $t("Default") }}</option>
<option value="arcade">{{ $t("Arcade") }}</option>
<option value="correct">{{ $t("Correct") }}</option>
<option value="fail">{{ $t("Fail") }}</option>
<option value="harp">{{ $t("Harp") }}</option>
<option value="reveal">{{ $t("Reveal") }}</option>
<option value="bubble">{{ $t("Bubble") }}</option>
<option value="doorbell">{{ $t("Doorbell") }}</option>
<option value="flute">{{ $t("Flute") }}</option>
<option value="money">{{ $t("Money") }}</option>
<option value="scifi">{{ $t("Scifi") }}</option>
<option value="clear">{{ $t("Clear") }}</option>
<option value="elevator">{{ $t("Elevator") }}</option>
<option value="guitar">{{ $t("Guitar") }}</option>
<option value="pop">{{ $t("Pop") }}</option>
</select>
<div class="form-text">
{{ $t("Custom sound to override default notification sound") }}
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input v-model="$parent.notification.pushTimeSensitive" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Time Sensitive (iOS Only)") }}</label>
</div>
<div class="form-text">
{{ $t("Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.") }}
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.push.techulus.com" target="_blank">https://docs.push.techulus.com</a>
</i18n-t>
@ -16,5 +63,19 @@ export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.pushTitle === "undefined") {
this.$parent.notification.pushTitle = "Uptime-Kuma";
}
if (typeof this.$parent.notification.pushChannel === "undefined") {
this.$parent.notification.pushChannel = "uptime-kuma";
}
if (typeof this.$parent.notification.pushSound === "undefined") {
this.$parent.notification.pushSound = "default";
}
if (typeof this.$parent.notification.pushTimeSensitive === "undefined") {
this.$parent.notification.pushTimeSensitive = true;
}
},
};
</script>

View file

@ -9,6 +9,7 @@ import CallMeBot from "./CallMeBot.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Elks from "./46elks.vue";
import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue";
@ -65,6 +66,7 @@ import Whapi from "./Whapi.vue";
import Cellsynt from "./Cellsynt.vue";
import WPush from "./WPush.vue";
import SIGNL4 from "./SIGNL4.vue";
import SendGrid from "./SendGrid.vue";
/**
* Manage all notification form.
@ -82,6 +84,7 @@ const NotificationFormList = {
"smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
"Elks": Elks,
"Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat,
@ -137,7 +140,8 @@ const NotificationFormList = {
"whapi": Whapi,
"gtxmessaging": GtxMessaging,
"Cellsynt": Cellsynt,
"WPush": WPush
"WPush": WPush,
"SendGrid": SendGrid,
};
export default NotificationFormList;

1
src/lang/ab.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1011,5 +1011,45 @@
"OAuth2: Client Credentials": "OAuth2: přihlašovací údaje klienta",
"Authentication Method": "Metoda ověřování",
"Authorization Header": "Hlavička autorizace",
"Form Data Body": "Tělo formuláře s daty"
"Form Data Body": "Tělo formuláře s daty",
"threemaRecipientTypePhoneFormat": "E.164, bez počátečního +",
"jsonQueryDescription": "Pro zpracování a získání konkrétních dat z JSON odpovědi serveru použijte JSON dotaz - případně \"$\" pro zdrojovou (raw) odpověď, pokud neočekáváte JSON výstup. Výsledek bude následně porovnán jako řetězec vůči očekávaní hodnotě. Dokumentaci naleznete na {0} a pro testování dotazů můžete využít {1}.",
"shrinkDatabaseDescriptionSqlite": "Podmínka spuštění příkazu {vacuum} nad SQLite databází. Příkaz {auto_vacuum} je již zapnutý, ale nedochází k defragmentaci databáze ani k přebalení jednotlivých stránek databáze tak, jak to dělá příkaz {vacuum}.",
"Community String": "Řetězec komunity",
"Host Onesender": "Onesender hostitel",
"Token Onesender": "Onesender token",
"snmpOIDHelptext": "Zadejte OID senzoru nebo stavu, který chcete monitorovat. Pokud si nejste jisti identifikátorem OID, použijte nástroje pro správu sítě, jako jsou prohlížeče MIB nebo SNMP software.",
"snmpCommunityStringHelptext": "Tento řetězec slouží jako heslo pro ověřování a řízení přístupu k zařízením podporujícím protokol SNMP. Shodujte se s konfigurací zařízení SNMP.",
"record": "záznam",
"Go back to home page.": "Vrátit se domovskou stránku.",
"No tags found.": "Nenalezeny žádné štítky.",
"Lost connection to the socket server.": "Ztraceno socketové spojení se serverem.",
"Cannot connect to the socket server.": "Nelze navázat socketové spojení se serverem.",
"SIGNL4": "SIGNL4",
"SIGNL4 Webhook URL": "URL adresa webhooku SIGNL4",
"signl4Docs": "Další informace související s konfigurací SIGNL4 a postup jak získat URL webhooku SIGNL4 naleznete na {0}.",
"Conditions": "Podmínky",
"conditionAdd": "Přidat podmínku",
"conditionDelete": "Vymazat podmínku",
"conditionAddGroup": "Přidat skupinu",
"conditionDeleteGroup": "Smazat skupinu",
"conditionValuePlaceholder": "Hodnota",
"equals": "rovná se",
"not equals": "nerovná se",
"contains": "obsahuje",
"not contains": "neobsahuje",
"starts with": "začíná na",
"not starts with": "nezačíná na",
"ends with": "končí na",
"not ends with": "nekončí na",
"less than": "menší než",
"greater than": "větší než",
"less than or equal to": "menší nebo rovno",
"greater than or equal to": "větší nebo rovno",
"groupOnesenderDesc": "Ujistěte se, že jste zadali platné GroupID. Pro odeslání zprávy do skupiny zadejte například 628123456789-342345",
"OAuth Token URL": "URL OAuth tokenu",
"Client ID": "ID klienta",
"Client Secret": "Tajemství klienta",
"OAuth Scope": "OAuth rozsah",
"Optional: Space separated list of scopes": "Volitelné: seznam rozsahů oddělte mezerami"
}

View file

@ -1,7 +1,7 @@
{
"languageName": "Deutsch (Schweiz)",
"Settings": "Einstellungen",
"Dashboard": "Dashboard",
"Dashboard": "Überblick",
"New Update": "Update verfügbar",
"Language": "Sprache",
"Appearance": "Erscheinungsbild",
@ -1047,5 +1047,6 @@
"greater than": "mehr als",
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "grösser als oder gleich",
"record": "Eintrag"
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
}

View file

@ -1,7 +1,7 @@
{
"languageName": "Deutsch",
"Settings": "Einstellungen",
"Dashboard": "Dashboard",
"Dashboard": "Überblick",
"New Update": "Aktualisierung verfügbar",
"Language": "Sprache",
"Appearance": "Erscheinungsbild",
@ -1050,5 +1050,6 @@
"less than": "weniger als",
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "größer als oder gleich",
"record": "Eintrag"
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
}

View file

@ -541,6 +541,8 @@
"smtpBCC": "BCC",
"Discord Webhook URL": "Discord Webhook URL",
"wayToGetDiscordURL": "You can get this by going to Server Settings -> Integrations -> View Webhooks -> New Webhook",
"wayToGetDiscordURLEnhanced": "Using the Discord API, you may also create the webhook {0} for button support.",
"withADiscordBot": "with a bot",
"Bot Display Name": "Bot Display Name",
"Prefix Custom Message": "Prefix Custom Message",
"Hello @everyone is...": "Hello {'@'}everyone is…",
@ -1027,5 +1029,38 @@
"greater than": "greater than",
"less than or equal to": "less than or equal to",
"greater than or equal to": "greater than or equal to",
"record": "record"
"record": "record",
"Notification Channel": "Notification Channel",
"Sound": "Sound",
"Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only",
"Arcade": "Arcade",
"Correct": "Correct",
"Fail":"Fail",
"Harp":"Harp",
"Reveal":"Reveal",
"Bubble":"Bubble",
"Doorbell":"Doorbell",
"Flute":"Flute",
"Money":"Money",
"Scifi":"Scifi",
"Clear":"Clear",
"Elevator":"Elevator",
"Guitar":"Guitar",
"Pop":"Pop",
"Custom sound to override default notification sound": "Custom sound to override default notification sound",
"Time Sensitive (iOS Only)": "Time Sensitive (iOS Only)",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.",
"From":"From",
"Can be found on:": "Can be found on: {0}",
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.",
"RabbitMQ Nodes": "RabbitMQ Management Nodes",
"rabbitmqNodesDescription": "Enter the URL for the RabbitMQ management nodes including protocol and port. Example: {0}",
"rabbitmqNodesRequired": "Please set the nodes for this monitor.",
"rabbitmqNodesInvalid": "Please use a fully qualified (starting with 'http') URL for RabbitMQ nodes.",
"RabbitMQ Username": "RabbitMQ Username",
"RabbitMQ Password": "RabbitMQ Password",
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas"
}

1
src/lang/enm.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -106,7 +106,7 @@
"disableauth.message2": "Egoera jakin batzuetarako diseinatuta dago, Uptime Kumaren {intendThirdPartyAuth} (Cloudflare Access, Authelia edo beste autentifikazio-mekanismo batzuk).",
"where you intend to implement third-party authentication": "aurrean hirugarrengo autentifikazio batzuek jartzeko",
"Please use this option carefully!": "Mesedez, kontuz erabili aukera hau!",
"Logout": "Saioa amaitu",
"Logout": "Itxi saioa",
"Leave": "Utzi",
"I understand, please disable": "Ulertzen dut, mesedez desgaitu",
"Confirm": "Baieztatu",
@ -115,7 +115,7 @@
"Username": "Erabiltzailea",
"Password": "Pasahitza",
"Remember me": "Gogora nazazu",
"Login": "Saioa hasi",
"Login": "Hasi saioa",
"No Monitors, please": "Monitorizaziorik ez, mesedez",
"add one": "gehitu bat",
"Notification Type": "Jakinarazpen mota",
@ -164,11 +164,11 @@
"Add New below or Select...": "Gehitu beste bat behean edo hautatu…",
"Tag with this name already exist.": "Izen hau duen etiketa dagoeneko badago.",
"Tag with this value already exist.": "Balio hau duen etiketa dagoeneko badago.",
"color": "kolorea",
"color": "Kolorea",
"value (optional)": "balioa (hautazkoa)",
"Gray": "Grisa",
"Red": "Gorria",
"Orange": "Naranja",
"Orange": "Laranja",
"Green": "Berdea",
"Blue": "Urdina",
"Indigo": "Indigo",
@ -190,7 +190,7 @@
"Status Page": "Egoera orria",
"Status Pages": "Egoera orriak",
"defaultNotificationName": "Nire {notification} Alerta ({number})",
"here": "Hemen",
"here": "hemen",
"Required": "Beharrezkoa",
"telegram": "Telegram",
"ZohoCliq": "ZohoCliq",
@ -582,6 +582,10 @@
"Mechanism": "Mekanismoa",
"Home": "Hasiera",
"filterActive": "Aktibo",
"filterActivePaused": "Geldituta",
"Expected Value": "Esperotako balioa"
"filterActivePaused": "Pausatua",
"Expected Value": "Esperotako balioa",
"statusPageRefreshIn": "{0} barru freskatuko da.",
"now": "orain",
"time ago": "duela {0}",
"-year": "-urte"
}

View file

@ -1050,5 +1050,6 @@
"greater than": "supérieur à",
"less than or equal to": "inférieur ou égal à",
"greater than or equal to": "supérieur ou égal à",
"record": "enregistrer"
"record": "enregistrer",
"shrinkDatabaseDescriptionSqlite": "Déclencher la commande {vacuum} pour la base de données SQLite. {auto_vacuum} est déjà activé, mais cela ne défragmente pas la base de données ni ne réorganise les pages individuelles de la base de données de la même manière que la commande {vacuum}."
}

View file

@ -1015,5 +1015,6 @@
"less than": "níos lú ná",
"greater than": "níos mó ná",
"less than or equal to": "níos lú ná nó cothrom le",
"record": "taifead"
"record": "taifead",
"shrinkDatabaseDescriptionSqlite": "Bunachar sonraí truicear {vacuum} le haghaidh SQLite. Tá {auto_vacuum} cumasaithe cheana féin ach ní dhéanann sé seo scoilt ar an mbunachar sonraí ná athphacáil leathanaigh aonair an bhunachair sonraí mar a dhéanann an t-ordú {vacuum}."
}

View file

@ -1045,5 +1045,6 @@
"New Group": "Grup Baru",
"Group Name": "Nama Grup",
"OAuth2: Client Credentials": "OAuth2: Kredensial Klien",
"Authentication Method": "Metode Autentikasi"
"Authentication Method": "Metode Autentikasi",
"shrinkDatabaseDescriptionSqlite": "Memicu pangkalan data {vacuum} untuk SQLite. {auto_vacuum} sudah diaktifkan, tetapi tidak mendefragmentasi pangkalan data atau mengemas ulang halaman individual dari pangkalan data seperti yang dilakukan oleh perintah {vacuum}."
}

View file

@ -989,5 +989,9 @@
"wayToGetThreemaGateway": "Możesz zarejestrować się w Threema Gateway {0}.",
"threemaSenderIdentityFormat": "8 znaków, zwykle zaczyna się od *",
"threemaBasicModeInfo": "Uwaga: Ta integracja korzysta z Threema Gateway w trybie podstawowym (szyfrowanie po stronie serwera). Więcej szczegółów można znaleźć {0}.",
"apiKeysDisabledMsg": "Klucze API są wyłączone, ponieważ wyłączone jest uwierzytelnianie."
"apiKeysDisabledMsg": "Klucze API są wyłączone, ponieważ wyłączone jest uwierzytelnianie.",
"-year": "-rok",
"and": "i",
"now": "teraz",
"cacheBusterParam": "Dodaj parametr {0}"
}

View file

@ -1050,5 +1050,6 @@
"less than": "daha küçük",
"greater than or equal to": "büyük veya eşit",
"record": "kayıt",
"jsonQueryDescription": "JSON sorgusunu kullanarak sunucunun JSON yanıtından belirli verileri ayrıştırın ve çıkarın. JSON beklemiyorsanız ham yanıt için \"$\" sembolünü kullanın. Sonuç daha sonra metin olarak beklenen değerle karşılaştırılır. Belgeler için {0}'a bakın ve sorgularla denemeler yapmak için {1}'i kullanın."
"jsonQueryDescription": "JSON sorgusunu kullanarak sunucunun JSON yanıtından belirli verileri ayrıştırın ve çıkarın. JSON beklemiyorsanız ham yanıt için \"$\" sembolünü kullanın. Sonuç daha sonra metin olarak beklenen değerle karşılaştırılır. Belgeler için {0}'a bakın ve sorgularla denemeler yapmak için {1}'i kullanın.",
"shrinkDatabaseDescriptionSqlite": "SQLite için {vacuum} veritabanını tetikle. {auto_vacuum} zaten etkin ancak bu, {vacuum} komutunun yaptığı gibi veritabanını birleştirmez veya tek tek veritabanı sayfalarını yeniden paketlemez."
}

1
src/lang/ug.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1056,5 +1056,6 @@
"greater than": "більше, ніж",
"less than or equal to": "менше або дорівнює",
"greater than or equal to": "більше або дорівнює",
"record": "запис"
"record": "запис",
"shrinkDatabaseDescriptionSqlite": "Запускає команду {vacuum} для бази даних SQLite. Команда {auto_vacuum} вже увімкнена, але вона не дефрагментує базу даних і не перепаковує окремі сторінки бази даних так, як це робить команда {vacuum}."
}

View file

@ -1052,5 +1052,6 @@
"less than or equal to": "不多于",
"greater than or equal to": "不少于",
"record": "记录",
"jsonQueryDescription": "使用 JSON 查询解析并提取服务器 JSON 响应中的特定数据,或者,如果不期望得到 JSON 响应,则可使用 \"$\" 获取原始响应。然后将结果转为字符串并与期望值进行字符串比较。有关更多文档,请参阅 {0},亦可使用 {1} 来尝试查询。"
"jsonQueryDescription": "使用 JSON 查询解析并提取服务器 JSON 响应中的特定数据,或者,如果不期望得到 JSON 响应,则可使用 \"$\" 获取原始响应。然后将结果转为字符串并与期望值进行字符串比较。有关更多文档,请参阅 {0},亦可使用 {1} 来尝试查询。",
"shrinkDatabaseDescriptionSqlite": "触发 SQLite 数据库的 {vacuum} 命令。{auto_vacuum} 已经启用,但它不会像 {vacuum} 命令那样对数据库进行碎片整理,也不会重新打包各个数据库页面。"
}

View file

@ -64,6 +64,9 @@
<option value="mqtt">
MQTT
</option>
<option value="rabbitmq">
RabbitMQ
</option>
<option value="kafka-producer">
Kafka Producer
</option>
@ -90,6 +93,13 @@
</option>
</optgroup>
</select>
<i18n-t v-if="monitor.type === 'rabbitmq'" keypath="rabbitmqHelpText" tag="div" class="form-text">
<template #rabitmq_documentation>
<a href="https://www.rabbitmq.com/management" target="_blank" rel="noopener noreferrer">
RabbitMQ documentation
</a>
</template>
</i18n-t>
</div>
<div v-if="monitor.type === 'tailscale-ping'" class="alert alert-warning" role="alert">
@ -233,6 +243,43 @@
</div>
</template>
<template v-if="monitor.type === 'rabbitmq'">
<!-- RabbitMQ Nodes List -->
<div class="my-3">
<label for="rabbitmqNodes" class="form-label">{{ $t("RabbitMQ Nodes") }}</label>
<VueMultiselect
id="rabbitmqNodes"
v-model="monitor.rabbitmqNodes"
:required="true"
:multiple="true"
:options="[]"
:placeholder="$t('Enter the list of nodes')"
:tag-placeholder="$t('Press Enter to add node')"
:max-height="500"
:taggable="true"
:show-no-options="false"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="false"
:preselect-first="false"
@tag="addRabbitmqNode"
></VueMultiselect>
<div class="form-text">
{{ $t("rabbitmqNodesDescription", ["https://node1.rabbitmq.com:15672"]) }}
</div>
</div>
<div class="my-3">
<label for="rabbitmqUsername" class="form-label">RabbitMQ {{ $t("RabbitMQ Username") }}</label>
<input id="rabbitmqUsername" v-model="monitor.rabbitmqUsername" type="text" required class="form-control">
</div>
<div class="my-3">
<label for="rabbitmqPassword" class="form-label">{{ $t("RabbitMQ Password") }}</label>
<HiddenInput id="rabbitmqPassword" v-model="monitor.rabbitmqPassword" autocomplete="false" required="true"></HiddenInput>
</div>
</template>
<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
@ -549,7 +596,7 @@
</div>
<!-- Timeout: HTTP / Keyword / SNMP only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp' || monitor.type === 'rabbitmq'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
@ -1122,6 +1169,9 @@ const monitorDefaults = {
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
remote_browser: null,
rabbitmqNodes: [],
rabbitmqUsername: "",
rabbitmqPassword: "",
conditions: []
};
@ -1709,6 +1759,10 @@ message HealthCheckResponse {
this.monitor.kafkaProducerBrokers.push(newBroker);
},
addRabbitmqNode(newNode) {
this.monitor.rabbitmqNodes.push(newNode);
},
/**
* Validate form input
* @returns {boolean} Is the form input valid?
@ -1736,6 +1790,17 @@ message HealthCheckResponse {
return false;
}
}
if (this.monitor.type === "rabbitmq") {
if (this.monitor.rabbitmqNodes.length === 0) {
toast.error(this.$t("rabbitmqNodesRequired"));
return false;
}
if (!this.monitor.rabbitmqNodes.every(node => node.startsWith("http://") || node.startsWith("https://"))) {
toast.error(this.$t("rabbitmqNodesInvalid"));
return false;
}
}
return true;
},

View file

@ -0,0 +1,102 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util");
/**
* Runs an MQTT test with the
* @param {string} mqttSuccessMessage the message that the monitor expects
* @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
* @param {string} receivedMessage what message is recieved from the mqtt channel
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
const monitor = {
jsonPath: "firstProp", // always return firstProp for the json-query monitor
hostname: connectionString.split(":", 2).join(":"),
mqttTopic: "test",
port: connectionString.split(":")[2],
mqttUsername: null,
mqttPassword: null,
interval: 20, // controls the timeout
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
testMqttClient.on("connect", () => {
testMqttClient.subscribe("test", (error) => {
if (!error) {
testMqttClient.publish("test", receivedMessage);
}
});
});
try {
await mqttMonitorType.check(monitor, heartbeat, {});
} finally {
testMqttClient.end();
hiveMQContainer.stop();
}
return heartbeat;
}
describe("MqttMonitorType", {
concurrency: true,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => {
test("valid keywords (type=default)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("valid keywords (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("invalid keywords (type=default)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("invalid keyword (type=keyword)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("valid json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
});
test("invalid (because query fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]"),
);
});
test("invalid (because successMessage fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
});

View file

@ -0,0 +1,53 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { RabbitMQContainer } = require("@testcontainers/rabbitmq");
const { RabbitMqMonitorType } = require("../../server/monitor-types/rabbitmq");
const { UP, DOWN, PENDING } = require("../../src/util");
describe("RabbitMQ Single Node", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
}, () => {
test("RabbitMQ is running", async () => {
// The default timeout of 30 seconds might not be enough for the container to start
const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start();
const rabbitMQMonitor = new RabbitMqMonitorType();
const connectionString = `http://${rabbitMQContainer.getHost()}:${rabbitMQContainer.getMappedPort(15672)}`;
const monitor = {
rabbitmqNodes: JSON.stringify([ connectionString ]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await rabbitMQMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "OK");
} finally {
rabbitMQContainer.stop();
}
});
test("RabbitMQ is not running", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]),
rabbitmqUsername: "rabbitmqUser",
rabbitmqPassword: "rabbitmqPass",
};
const heartbeat = {
msg: "",
status: PENDING,
};
await rabbitMQMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, DOWN);
});
});