Compare commits
108 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6ccf741bc4 | ||
|
0a58069742 | ||
|
2b57b3e863 | ||
|
6cd6a2edf0 | ||
|
6961b1bdd2 | ||
|
54d4c4d3f7 | ||
|
c47b6c5995 | ||
|
7ef404ccc1 | ||
|
a5ff27da7a | ||
|
7bb12a7e00 | ||
|
27585d0812 | ||
|
e675316635 | ||
|
b073ec2287 | ||
|
31f45dcfc9 | ||
|
49ac71e25c | ||
|
1a9b013fc2 | ||
|
1326761a8a | ||
|
e48a987b9c | ||
|
712a3c29d4 | ||
|
e9497ac1ab | ||
|
6437ef198f | ||
|
973d5692d0 | ||
|
468cb004d6 | ||
|
f7d41a30fa | ||
|
0ef686ac2f | ||
|
3b5893ea60 | ||
|
21cd4d64c3 | ||
|
db757123ba | ||
|
4dcf31621e | ||
|
9c1ba97e7d | ||
|
e9564619f1 | ||
|
8433bceb32 | ||
|
98d001b38b | ||
|
d9f12a6376 | ||
|
56ba133a1f | ||
|
ec30147a7f | ||
|
2bc165379a | ||
|
2172112144 | ||
|
ecd661c801 | ||
|
8fab7112a1 | ||
|
cc4ed308b0 | ||
|
362280af14 | ||
|
636fc8fcfc | ||
|
1c05ba09dc | ||
|
1565da87cf | ||
|
890e3abf58 | ||
|
33355c51b7 | ||
|
5f5c2d7c46 | ||
|
71f4ab0aa6 | ||
|
24d1dd4c34 | ||
|
c00abac834 | ||
|
439f963749 | ||
|
f15c6470af | ||
|
32f7a0084a | ||
|
f8658d6160 | ||
|
d596f8f7eb | ||
|
23a525e36a | ||
|
221d1d40f5 | ||
|
60ec87941e | ||
|
e8e4361e09 | ||
|
675806829c | ||
|
21c1921867 | ||
|
e490ec6d29 | ||
|
7ad4392529 | ||
|
f3d3e064f8 | ||
|
80c91b8234 | ||
|
dc8289df12 | ||
|
caff9ca736 | ||
|
c7eb72e73b | ||
|
fc5ec5f492 | ||
|
40ebc2df79 | ||
|
abf5e435fe | ||
|
8a372201f1 | ||
|
8ec240fe19 | ||
|
5362aab0e5 | ||
|
bc7271b99c | ||
|
4ebf5a5f07 | ||
|
fbceefec36 | ||
|
0b959514f8 | ||
|
4c5e2ea237 | ||
|
7d92351568 | ||
|
494c53971c | ||
|
fc1914bccd | ||
|
4239cf4255 | ||
|
c196c34840 | ||
|
554402b484 | ||
|
b049e4e1b4 | ||
|
90a2668272 | ||
|
49b5de7d40 | ||
|
69e1880cd3 | ||
|
610b6248aa | ||
|
e591647b60 | ||
|
da99833d4c | ||
|
4bf23fdd1a | ||
|
f99a64da67 | ||
|
2e022789f6 | ||
|
73835f3328 | ||
|
4819112e25 | ||
|
2d3fd738e4 | ||
|
edd8fe2e22 | ||
|
204792dd2d | ||
|
3ea6711053 | ||
|
f8c7da7995 | ||
|
f3660a0cec | ||
|
0e30843a75 | ||
|
2103edb604 | ||
|
b059a36e66 | ||
|
6b47ad07ca |
18
.github/workflows/auto-test.yml
vendored
18
.github/workflows/auto-test.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node: [ 14, 16, 17, 18 ]
|
||||
node: [ 14, 16, 18, 19 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@ -66,3 +66,19 @@ jobs:
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:test
|
||||
|
||||
frontend-unit-tests:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:run:unit
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Project Info
|
||||
|
||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that.
|
||||
First of all, I want to thank everyone who made 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. It is not very well structured or commented, sorry about that.
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
||||
|
||||
## Can I create a pull request for Uptime Kuma?
|
||||
|
||||
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
|
||||
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know it will be merged or not.
|
||||
|
||||
Here are some references:
|
||||
|
||||
@ -48,8 +48,13 @@ Here are some references:
|
||||
- UI/UX is not close to Uptime Kuma
|
||||
- Existing logic is completely modified or deleted for no reason
|
||||
- A function that is completely out of scope
|
||||
- Convert existing code into other programming languages
|
||||
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
|
||||
|
||||
The above cases cannot cover all situations.
|
||||
|
||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
||||
|
||||
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
|
||||
|
||||
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
|
||||
@ -72,9 +77,9 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
|
||||
|
||||
## Project Styles
|
||||
|
||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||
I personally do not like it when something requires so much learning and configuration before you can finally start the app.
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort required to get it running
|
||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
|
||||
- Easy to use
|
||||
@ -172,15 +177,11 @@ The data and socket logic are in `src/mixins/socket.js`.
|
||||
|
||||
## Unit Test
|
||||
|
||||
It is an end-to-end testing. It is using Jest and Puppeteer.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So:
|
||||
|
30
README.md
30
README.md
@ -7,9 +7,9 @@
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||
|
||||
<img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
|
||||
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
||||
|
||||
## 🥔 Live Demo
|
||||
|
||||
@ -22,17 +22,17 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||
* 20 second intervals.
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||
* Fancy, Reactive, Fast UI/UX
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
||||
* 20 second intervals
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||
* Multiple Status Pages
|
||||
* Map Status Page to Domain
|
||||
* Ping Chart
|
||||
* Certificate Info
|
||||
* Proxy Support
|
||||
* 2FA available
|
||||
* Multiple status pages
|
||||
* Map status pages to specific domains
|
||||
* Ping chart
|
||||
* Certificate info
|
||||
* Proxy support
|
||||
* 2FA support
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
@ -44,14 +44,14 @@ docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name upti
|
||||
|
||||
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
|
||||
|
||||
Browse to http://localhost:3001 after starting.
|
||||
Uptime Kuma is now running on http://localhost:3001
|
||||
|
||||
### 💪🏻 Non-Docker
|
||||
|
||||
Required Tools:
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) - For run in background
|
||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||
|
||||
```bash
|
||||
# Update your npm to the latest version
|
||||
@ -73,7 +73,7 @@ pm2 start server/server.js --name uptime-kuma
|
||||
|
||||
|
||||
```
|
||||
Browse to http://localhost:3001 after starting.
|
||||
Uptime Kuma is now running on http://localhost:3001
|
||||
|
||||
More useful PM2 Commands
|
||||
|
||||
|
10
config/cypress.frontend.config.js
Normal file
10
config/cypress.frontend.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
supportFile: false,
|
||||
specPattern: [
|
||||
"test/cypress/unit/**/*.js"
|
||||
],
|
||||
}
|
||||
});
|
@ -4,5 +4,5 @@ WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise==1.2.0 && \
|
||||
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||
rm -rf /root/.cache
|
||||
|
16
docker/builder-go.dockerfile
Normal file
16
docker/builder-go.dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
############################################
|
||||
# Build in Golang
|
||||
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
|
||||
############################################
|
||||
FROM golang:1.19.4-buster
|
||||
WORKDIR /app
|
||||
ARG TARGETPLATFORM
|
||||
COPY ./extra/ ./extra/
|
||||
|
||||
# Compile healthcheck.go
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install curl && \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
|
||||
apt --yes --no-install-recommends install nodejs && \
|
||||
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
|
||||
apt --yes remove nodejs
|
@ -11,7 +11,7 @@ WORKDIR /app
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init && \
|
||||
pip3 --no-cache-dir install apprise==1.2.0 && \
|
||||
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
|
@ -1,19 +1,9 @@
|
||||
############################################
|
||||
# Build in Golang
|
||||
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
|
||||
# Check file: builder-go.dockerfile
|
||||
############################################
|
||||
FROM golang:1.19.4-buster AS build_healthcheck
|
||||
WORKDIR /app
|
||||
ARG TARGETPLATFORM
|
||||
COPY ./extra/ ./extra/
|
||||
|
||||
# Compile healthcheck.go
|
||||
RUN apt update
|
||||
RUN apt --yes --no-install-recommends install curl
|
||||
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash
|
||||
RUN apt --yes --no-install-recommends install nodejs
|
||||
RUN node -v
|
||||
RUN node ./extra/build-healthcheck.js $TARGETPLATFORM
|
||||
FROM louislam/uptime-kuma:builder-go AS build_healthcheck
|
||||
|
||||
############################################
|
||||
# Build in Node.js
|
||||
@ -22,10 +12,13 @@ FROM louislam/uptime-kuma:base-debian AS build
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
COPY .npmrc .npmrc
|
||||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
COPY --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck
|
||||
RUN npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
RUN chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
############################################
|
||||
# ⭐ Main Image
|
||||
|
@ -3,10 +3,12 @@ WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY .npmrc .npmrc
|
||||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
RUN npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
RUN chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
FROM louislam/uptime-kuma:base-alpine AS release
|
||||
WORKDIR /app
|
||||
|
@ -32,6 +32,10 @@ if (! exists) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit updated files
|
||||
* @param {string} version Version to update to
|
||||
*/
|
||||
function commit(version) {
|
||||
let msg = "Update to " + version;
|
||||
|
||||
@ -47,6 +51,10 @@ function commit(version) {
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag with the specified version
|
||||
* @param {string} version Tag to create
|
||||
*/
|
||||
function tag(version) {
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
@ -55,6 +63,11 @@ function tag(version) {
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag exists for the specified version
|
||||
* @param {string} version Version to check
|
||||
* @returns {boolean} Does the tag already exist
|
||||
*/
|
||||
function tagExists(version) {
|
||||
if (! version) {
|
||||
throw new Error("invalid version");
|
||||
|
@ -25,6 +25,10 @@ if (platform === "linux/amd64") {
|
||||
const file = fs.createWriteStream("cloudflared.deb");
|
||||
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||
|
||||
/**
|
||||
* Download specified file
|
||||
* @param {string} url URL to request
|
||||
*/
|
||||
function get(url) {
|
||||
http.get(url, function (res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
|
@ -1,3 +1,7 @@
|
||||
/*
|
||||
* If changed, have to run `npm run build-docker-builder-go`.
|
||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -43,6 +43,11 @@ const main = async () => {
|
||||
console.log("Finished.");
|
||||
};
|
||||
|
||||
/**
|
||||
* Ask question of user
|
||||
* @param {string} question Question to ask
|
||||
* @returns {Promise<string>} Users response
|
||||
*/
|
||||
function question(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
|
@ -53,6 +53,11 @@ const main = async () => {
|
||||
console.log("Finished.");
|
||||
};
|
||||
|
||||
/**
|
||||
* Ask question of user
|
||||
* @param {string} question Question to ask
|
||||
* @returns {Promise<string>} Users response
|
||||
*/
|
||||
function question(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
|
@ -135,6 +135,11 @@ server.listen({
|
||||
udp: 5300
|
||||
});
|
||||
|
||||
/**
|
||||
* Get human readable request type from request code
|
||||
* @param {number} code Request code to translate
|
||||
* @returns {string} Human readable request type
|
||||
*/
|
||||
function type(code) {
|
||||
for (let name in Packet.TYPE) {
|
||||
if (Packet.TYPE[name] === code) {
|
||||
|
@ -11,6 +11,7 @@ class SimpleMqttServer {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/** Start the MQTT server */
|
||||
start() {
|
||||
this.server.listen(this.port, () => {
|
||||
console.log("server started and listening on port ", this.port);
|
||||
|
@ -36,10 +36,8 @@ if (! exists) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the version number in package.json and commits it to git.
|
||||
* @param {string} version - The new version number
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Commit updated files
|
||||
* @param {string} version Version to update to
|
||||
*/
|
||||
function commit(version) {
|
||||
let msg = "Update to " + version;
|
||||
@ -53,16 +51,19 @@ function commit(version) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag with the specified version
|
||||
* @param {string} version Tag to create
|
||||
*/
|
||||
function tag(version) {
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given version is already tagged in the git repository.
|
||||
* @param {string} version - The version to check for.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Check if a tag exists for the specified version
|
||||
* @param {string} version Version to check
|
||||
* @returns {boolean} Does the tag already exist
|
||||
*/
|
||||
function tagExists(version) {
|
||||
if (! version) {
|
||||
|
@ -10,6 +10,10 @@ if (!newVersion) {
|
||||
|
||||
updateWiki(newVersion);
|
||||
|
||||
/**
|
||||
* Update the wiki with new version number
|
||||
* @param {string} newVersion Version to update to
|
||||
*/
|
||||
function updateWiki(newVersion) {
|
||||
const wikiDir = "./tmp/wiki";
|
||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||
@ -39,6 +43,10 @@ function updateWiki(newVersion) {
|
||||
safeDelete(wikiDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory exists and then delete it
|
||||
* @param {string} dir Directory to delete
|
||||
*/
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rm(dir, {
|
||||
|
4821
package-lock.json
generated
4821
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.19.3",
|
||||
"version": "1.19.6",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -31,6 +31,7 @@
|
||||
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
|
||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
||||
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
@ -38,7 +39,7 @@
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.19.3 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.19.6 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@ -60,11 +61,13 @@
|
||||
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
|
||||
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
|
||||
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.7.3",
|
||||
"@louislam/ping": "~0.4.2-mod.1",
|
||||
"@louislam/sqlite3": "15.1.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.27.0",
|
||||
@ -90,9 +93,10 @@
|
||||
"https-proxy-agent": "~5.0.1",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"limiter": "~2.1.0",
|
||||
"mongodb": "~4.13.0",
|
||||
"mqtt": "~4.3.7",
|
||||
"mssql": "~8.1.4",
|
||||
"mysql2": "~2.3.3",
|
||||
@ -106,7 +110,8 @@
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"protobufjs": "~7.1.1",
|
||||
"redbean-node": "0.1.4",
|
||||
"redbean-node": "~0.2.0",
|
||||
"redis": "~4.5.1",
|
||||
"socket.io": "~4.5.3",
|
||||
"socket.io-client": "~4.5.3",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
|
@ -63,6 +63,12 @@ function myAuthorizer(username, password, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use basic auth if auth is not disabled
|
||||
* @param {express.Request} req Express request object
|
||||
* @param {express.Response} res Express response object
|
||||
* @param {express.NextFunction} next
|
||||
*/
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
|
@ -37,6 +37,10 @@ class CacheableDnsHttpAgent {
|
||||
this.enable = isEnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach cacheable to HTTP agent
|
||||
* @param {http.Agent} agent Agent to install
|
||||
*/
|
||||
static install(agent) {
|
||||
this.cacheable.install(agent);
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ const initBackgroundJobs = function (args) {
|
||||
return bree;
|
||||
};
|
||||
|
||||
/** Stop all background jobs if running */
|
||||
const stopBackgroundJobs = function () {
|
||||
if (bree) {
|
||||
bree.stop();
|
||||
|
@ -25,6 +25,10 @@ const DEFAULT_KEEP_PERIOD = 180;
|
||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
if (parsedPeriod < 1) {
|
||||
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||
} else {
|
||||
|
||||
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||
|
||||
try {
|
||||
@ -35,6 +39,7 @@ const DEFAULT_KEEP_PERIOD = 180;
|
||||
} catch (e) {
|
||||
log(`Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
exit();
|
||||
})();
|
||||
|
@ -112,6 +112,11 @@ class Maintenance extends BeanModel {
|
||||
return this.toPublicJSON(timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of weekdays that the maintenance is active for
|
||||
* Monday=1, Tuesday=2 etc.
|
||||
* @returns {number[]} Array of active weekdays
|
||||
*/
|
||||
getDayOfWeekList() {
|
||||
log.debug("timeslot", "List: " + this.weekdays);
|
||||
return JSON.parse(this.weekdays).sort(function (a, b) {
|
||||
@ -119,12 +124,20 @@ class Maintenance extends BeanModel {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of days in month that maintenance is active for
|
||||
* @returns {number[]} Array of active days in month
|
||||
*/
|
||||
getDayOfMonthList() {
|
||||
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start date and time for maintenance
|
||||
* @returns {dayjs.Dayjs} Start date and time
|
||||
*/
|
||||
getStartDateTime() {
|
||||
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
|
||||
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
|
||||
@ -137,6 +150,10 @@ class Maintenance extends BeanModel {
|
||||
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duraction of maintenance in seconds
|
||||
* @returns {number} Duration of maintenance
|
||||
*/
|
||||
getDuration() {
|
||||
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||
// Add 24hours if it is across day
|
||||
@ -146,6 +163,12 @@ class Maintenance extends BeanModel {
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert data from socket to bean
|
||||
* @param {Bean} bean Bean to fill in
|
||||
* @param {Object} obj Data to fill bean with
|
||||
* @returns {Bean} Filled bean
|
||||
*/
|
||||
static jsonToBean(bean, obj) {
|
||||
if (obj.id) {
|
||||
bean.id = obj.id;
|
||||
|
@ -6,6 +6,11 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
class MaintenanceTimeslot extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
|
||||
@ -21,6 +26,10 @@ class MaintenanceTimeslot extends BeanModel {
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON() {
|
||||
return await this.toPublicJSON();
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing,
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
@ -36,7 +38,6 @@ class Monitor extends BeanModel {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
sendUrl: this.sendUrl,
|
||||
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||
};
|
||||
|
||||
if (this.sendUrl) {
|
||||
@ -494,14 +495,18 @@ class Monitor extends BeanModel {
|
||||
|
||||
const options = {
|
||||
url: `/containers/${this.docker_container}/json`,
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
if (dockerHost._dockerType === "socket") {
|
||||
@ -514,7 +519,9 @@ class Monitor extends BeanModel {
|
||||
let res = await axios.request(options);
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
bean.msg = "";
|
||||
bean.msg = res.data.State.Status;
|
||||
} else {
|
||||
throw Error("Container State is " + res.data.State.Status);
|
||||
}
|
||||
} else if (this.type === "mqtt") {
|
||||
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
||||
@ -579,6 +586,15 @@ class Monitor extends BeanModel {
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mongodbPing(this.databaseConnectionString);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
@ -615,6 +631,12 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
@ -746,6 +768,13 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request using axios
|
||||
* @param {Object} options Options for Axios
|
||||
* @param {boolean} finalCall Should this be the final call i.e
|
||||
* don't retry on faliure
|
||||
* @returns {Object} Axios response
|
||||
*/
|
||||
async makeAxiosRequest(options, finalCall = false) {
|
||||
try {
|
||||
let res;
|
||||
@ -1227,6 +1256,7 @@ class Monitor extends BeanModel {
|
||||
return maintenance.count !== 0;
|
||||
}
|
||||
|
||||
/** Make sure monitor interval is between bounds */
|
||||
validate() {
|
||||
if (this.interval > MAX_INTERVAL_SECOND) {
|
||||
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
|
||||
|
@ -8,6 +8,14 @@ class PromoSMS extends NotificationProvider {
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
if (notification.promosmsAllowLongSMS === undefined) {
|
||||
notification.promosmsAllowLongSMS = false;
|
||||
}
|
||||
|
||||
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
|
||||
//Lets remove non ascii char
|
||||
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
@ -18,8 +26,9 @@ class PromoSMS extends NotificationProvider {
|
||||
};
|
||||
let data = {
|
||||
"recipients": [ notification.promosmsPhoneNumber ],
|
||||
//Lets remove non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
//Trim message to maximum length of 1 SMS or 4 if we allowed long messages
|
||||
"text": notification.promosmsAllowLongSMS ? cleanMsg.substring(0, 639) : cleanMsg.substring(0, 159),
|
||||
"long-sms": notification.promosmsAllowLongSMS,
|
||||
"type": Number(notification.promosmsSMSType),
|
||||
"sender": notification.promosmsSenderName
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ class Pushover extends NotificationProvider {
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
|
||||
"message": "<b>Message</b>:" + msg,
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
|
@ -21,6 +21,12 @@ class ServerChan extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted title for message
|
||||
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {string} Formatted title
|
||||
*/
|
||||
checkStatus(heartbeatJSON, monitorJSON) {
|
||||
let title = "UptimeKuma Message";
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
|
113
server/notification-providers/splunk.js
Normal file
113
server/notification-providers/splunk.js
Normal file
@ -0,0 +1,113 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
let successMessage = "Sent Successfully.";
|
||||
|
||||
class Splunk extends NotificationProvider {
|
||||
name = "Splunk";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
const title = "Uptime Kuma Alert";
|
||||
const monitor = {
|
||||
type: "ping",
|
||||
url: "Uptime Kuma Test Button",
|
||||
};
|
||||
return this.postNotification(notification, title, msg, monitor, "trigger");
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === UP) {
|
||||
const title = "Uptime Kuma Monitor ✅ Up";
|
||||
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "recovery");
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if result is successful, result code should be in range 2xx
|
||||
* @param {Object} result Axios response object
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
if (result.status == null) {
|
||||
throw new Error("Splunk notification failed with invalid response!");
|
||||
}
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
throw new Error("Splunk notification failed with status code " + result.status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message title
|
||||
* @param {string} body Message
|
||||
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||
* @returns {string}
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||
|
||||
let monitorUrl;
|
||||
if (monitorInfo.type === "port") {
|
||||
monitorUrl = monitorInfo.hostname;
|
||||
if (monitorInfo.port) {
|
||||
monitorUrl += ":" + monitorInfo.port;
|
||||
}
|
||||
} else if (monitorInfo.hostname != null) {
|
||||
monitorUrl = monitorInfo.hostname;
|
||||
} else {
|
||||
monitorUrl = monitorInfo.url;
|
||||
}
|
||||
|
||||
if (eventAction === "recovery") {
|
||||
if (notification.splunkAutoResolve === "0") {
|
||||
return "No action required";
|
||||
}
|
||||
eventAction = notification.splunkAutoResolve;
|
||||
} else {
|
||||
eventAction = notification.splunkSeverity;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: notification.splunkRestURL,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
data: {
|
||||
message_type: eventAction,
|
||||
state_message: `[${title}] [${monitorUrl}] ${body}`,
|
||||
entity_display_name: "Uptime Kuma Alert: " + monitorInfo.name,
|
||||
routing_key: notification.pagerdutyIntegrationKey,
|
||||
entity_id: "Uptime Kuma/" + monitorInfo.id,
|
||||
}
|
||||
};
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorInfo) {
|
||||
options.client = "Uptime Kuma";
|
||||
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||
}
|
||||
|
||||
let result = await axios.request(options);
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
return "Splunk notification succeed: " + result.statusText;
|
||||
}
|
||||
|
||||
return successMessage;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Splunk;
|
@ -40,6 +40,7 @@ const Stackfield = require("./notification-providers/stackfield");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const TechulusPush = require("./notification-providers/techulus-push");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Splunk = require("./notification-providers/splunk");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const WeCom = require("./notification-providers/wecom");
|
||||
const GoAlert = require("./notification-providers/goalert");
|
||||
@ -100,6 +101,7 @@ class Notification {
|
||||
new Teams(),
|
||||
new TechulusPush(),
|
||||
new Telegram(),
|
||||
new Splunk(),
|
||||
new Webhook(),
|
||||
new WeCom(),
|
||||
new GoAlert(),
|
||||
|
@ -1,199 +0,0 @@
|
||||
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
|
||||
// Fixed on Windows
|
||||
const net = require("net");
|
||||
const spawn = require("child_process").spawn;
|
||||
const events = require("events");
|
||||
const fs = require("fs");
|
||||
const util = require("./util-server");
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
/**
|
||||
* Constructor for ping class
|
||||
* @param {string} host Host to ping
|
||||
* @param {object} [options] Options for the ping command
|
||||
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
||||
*/
|
||||
function Ping(host, options) {
|
||||
if (!host) {
|
||||
throw new Error("You must specify a host to ping!");
|
||||
}
|
||||
|
||||
this._host = host;
|
||||
this._options = options = (options || {});
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
const timeout = 10;
|
||||
|
||||
if (util.WIN) {
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||
|
||||
} else if (util.LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
defaultArgs.unshift("-6");
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (util.MAC) {
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
this._bin = "/sbin/ping6";
|
||||
} else {
|
||||
this._bin = "/sbin/ping";
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (util.BSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
defaultArgs.unshift("-6");
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else {
|
||||
throw new Error("Could not detect your ping binary.");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this._bin)) {
|
||||
throw new Error("Could not detect " + this._bin + " on your system");
|
||||
}
|
||||
|
||||
this._i = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Callback for send
|
||||
* @callback pingCB
|
||||
* @param {any} err Any error encountered
|
||||
* @param {number} ms Ping time in ms
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a ping
|
||||
* @param {pingCB} callback Callback to call with results
|
||||
*/
|
||||
Ping.prototype.send = function (callback) {
|
||||
let self = this;
|
||||
callback = callback || function (err, ms) {
|
||||
if (err) {
|
||||
return self.emit("error", err);
|
||||
}
|
||||
return self.emit("result", ms);
|
||||
};
|
||||
|
||||
let _ended;
|
||||
let _exited;
|
||||
let _errored;
|
||||
|
||||
this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
|
||||
|
||||
this._ping.on("error", function (err) { // handle binary errors
|
||||
_errored = true;
|
||||
callback(err);
|
||||
});
|
||||
|
||||
this._ping.stdout.on("data", function (data) { // log stdout
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
this._ping.stdout.on("end", function () {
|
||||
_ended = true;
|
||||
if (_exited && !_errored) {
|
||||
onEnd.call(self._ping);
|
||||
}
|
||||
});
|
||||
|
||||
this._ping.stderr.on("data", function (data) { // log stderr
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
this._ping.on("exit", function (code) { // handle complete
|
||||
_exited = true;
|
||||
if (_ended && !_errored) {
|
||||
onEnd.call(self._ping);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Function} callback
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function onEnd() {
|
||||
let stdout = this.stdout._stdout;
|
||||
let stderr = this.stderr._stderr;
|
||||
let ms;
|
||||
|
||||
if (stderr) {
|
||||
return callback(new Error(stderr));
|
||||
}
|
||||
|
||||
if (!stdout) {
|
||||
return callback(new Error("No stdout detected"));
|
||||
}
|
||||
|
||||
ms = stdout.match(self._regmatch); // parse out the ##ms response
|
||||
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
|
||||
|
||||
callback(null, ms, stdout);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ping every interval
|
||||
* @param {pingCB} callback Callback to call with results
|
||||
*/
|
||||
Ping.prototype.start = function (callback) {
|
||||
let self = this;
|
||||
this._i = setInterval(function () {
|
||||
self.send(callback);
|
||||
}, (self._options.interval || 5000));
|
||||
self.send(callback);
|
||||
};
|
||||
|
||||
/** Stop sending pings */
|
||||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||
* Thank @pemassi
|
||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||
* @param {any} data
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertOutput(data) {
|
||||
if (util.WIN) {
|
||||
if (data) {
|
||||
return util.convertToUTF8(data);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
@ -99,6 +99,7 @@ class Prometheus {
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove monitor from prometheus */
|
||||
remove() {
|
||||
try {
|
||||
monitorCertDaysRemaining.remove(this.monitorLabelValues);
|
||||
|
@ -941,13 +941,21 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]);
|
||||
let bean = await R.findOne("tag", " id = ? ", [ tag.id ]);
|
||||
if (bean == null) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Tag not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
bean.name = tag.name;
|
||||
bean.color = tag.color;
|
||||
await R.store(bean);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
tag: await bean.toJSON(),
|
||||
});
|
||||
|
||||
|
@ -6,10 +6,10 @@ class UptimeCacheList {
|
||||
static list = {};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param monitorID
|
||||
* @param duration
|
||||
* @return number
|
||||
* Get the uptime for a specific period
|
||||
* @param {number} monitorID
|
||||
* @param {number} duration
|
||||
* @return {number}
|
||||
*/
|
||||
static getUptime(monitorID, duration) {
|
||||
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
|
||||
@ -20,6 +20,12 @@ class UptimeCacheList {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add uptime for specified monitor
|
||||
* @param {number} monitorID
|
||||
* @param {number} duration
|
||||
* @param {number} uptime Uptime to add
|
||||
*/
|
||||
static addUptime(monitorID, duration, uptime) {
|
||||
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
|
||||
if (!UptimeCacheList.list[monitorID]) {
|
||||
@ -28,6 +34,10 @@ class UptimeCacheList {
|
||||
UptimeCacheList.list[monitorID][duration] = uptime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for specified monitor
|
||||
* @param {number} monitorID
|
||||
*/
|
||||
static clearCache(monitorID) {
|
||||
log.debug("UptimeCacheList", "clearCache: " + monitorID);
|
||||
delete UptimeCacheList.list[monitorID];
|
||||
|
@ -86,6 +86,7 @@ class UptimeKumaServer {
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
/** Initialise app after the database has been set up */
|
||||
async initAfterDatabaseReady() {
|
||||
await CacheableDnsHttpAgent.update();
|
||||
|
||||
@ -98,6 +99,11 @@ class UptimeKumaServer {
|
||||
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send list of monitors to client
|
||||
* @param {Socket} socket
|
||||
* @returns {Object} List of monitors
|
||||
*/
|
||||
async sendMonitorList(socket) {
|
||||
let list = await this.getMonitorJSONList(socket.userID);
|
||||
this.io.to(socket.userID).emit("monitorList", list);
|
||||
@ -134,6 +140,11 @@ class UptimeKumaServer {
|
||||
return await this.sendMaintenanceListByUserID(socket.userID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send list of maintenances to user
|
||||
* @param {number} userID
|
||||
* @returns {Object}
|
||||
*/
|
||||
async sendMaintenanceListByUserID(userID) {
|
||||
let list = await this.getMaintenanceJSONList(userID);
|
||||
this.io.to(userID).emit("maintenanceList", list);
|
||||
@ -185,6 +196,11 @@ class UptimeKumaServer {
|
||||
errorLogStream.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IP of the client connected to the socket
|
||||
* @param {Socket} socket
|
||||
* @returns {string}
|
||||
*/
|
||||
async getClientIP(socket) {
|
||||
let clientIP = socket.client.conn.remoteAddress;
|
||||
|
||||
@ -203,6 +219,12 @@ class UptimeKumaServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get the current server timezone
|
||||
* If this fails, fall back to environment variables and then make a
|
||||
* guess.
|
||||
* @returns {string}
|
||||
*/
|
||||
async getTimezone() {
|
||||
let timezone = await Settings.get("serverTimezone");
|
||||
if (timezone) {
|
||||
@ -214,16 +236,25 @@ class UptimeKumaServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current offset
|
||||
* @returns {string}
|
||||
*/
|
||||
getTimezoneOffset() {
|
||||
return dayjs().format("Z");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current server timezone and environment variables
|
||||
* @param {string} timezone
|
||||
*/
|
||||
async setTimezone(timezone) {
|
||||
await Settings.set("serverTimezone", timezone, "general");
|
||||
process.env.TZ = timezone;
|
||||
dayjs.tz.setDefault(timezone);
|
||||
}
|
||||
|
||||
/** Load the timeslots for maintenance */
|
||||
async generateMaintenanceTimeslots() {
|
||||
|
||||
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
|
||||
@ -237,6 +268,7 @@ class UptimeKumaServer {
|
||||
|
||||
}
|
||||
|
||||
/** Stop the server */
|
||||
async stop() {
|
||||
clearTimeout(this.generateMaintenanceTimeslotsInterval);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const Ping = require("./ping-lite");
|
||||
const ping = require("@louislam/ping");
|
||||
const { R } = require("redbean-node");
|
||||
const { log, genSecret } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
@ -14,11 +14,13 @@ const mssql = require("mssql");
|
||||
const { Client } = require("pg");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
const mysql = require("mysql2");
|
||||
const { MongoClient } = require("mongodb");
|
||||
const { NtlmClient } = require("axios-ntlm");
|
||||
const { Settings } = require("./settings");
|
||||
const grpc = require("@grpc/grpc-js");
|
||||
const protojs = require("protobufjs");
|
||||
const radiusClient = require("node-radius-client");
|
||||
const redis = require("redis");
|
||||
const {
|
||||
dictionaries: {
|
||||
rfc2865: { file, attributes },
|
||||
@ -26,12 +28,7 @@ const {
|
||||
} = require("node-radius-utils");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
exports.LIN = /^linux/.test(process.platform);
|
||||
exports.MAC = /^darwin/.test(process.platform);
|
||||
exports.FBSD = /^freebsd/.test(process.platform);
|
||||
exports.BSD = /bsd$/.test(process.platform);
|
||||
const isWindows = process.platform === /^win/.test(process.platform);
|
||||
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
@ -105,18 +102,23 @@ exports.ping = async (hostname) => {
|
||||
*/
|
||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ping = new Ping(hostname, {
|
||||
ipv6
|
||||
});
|
||||
|
||||
ping.send(function (err, ms, stdout) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (ms === null) {
|
||||
reject(new Error(stdout));
|
||||
ping.promise.probe(hostname, {
|
||||
v6: ipv6,
|
||||
min_reply: 1,
|
||||
deadline: 10,
|
||||
}).then((res) => {
|
||||
// If ping failed, it will set field to unknown
|
||||
if (res.alive) {
|
||||
resolve(res.time);
|
||||
} else {
|
||||
resolve(Math.round(ms));
|
||||
if (isWindows) {
|
||||
reject(new Error(exports.convertToUTF8(res.output)));
|
||||
} else {
|
||||
reject(new Error(res.output));
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -135,7 +137,7 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
const { port, username, password, interval = 20 } = options;
|
||||
|
||||
// Adds MQTT protocol to the hostname if not already present
|
||||
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) {
|
||||
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
|
||||
hostname = "mqtt://" + hostname;
|
||||
}
|
||||
|
||||
@ -145,10 +147,11 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
reject(new Error("Timeout"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
log.debug("mqtt", "MQTT connecting");
|
||||
const mqttUrl = `${hostname}:${port}`;
|
||||
|
||||
let client = mqtt.connect(hostname, {
|
||||
port,
|
||||
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
|
||||
|
||||
let client = mqtt.connect(mqttUrl, {
|
||||
username,
|
||||
password
|
||||
});
|
||||
@ -280,18 +283,23 @@ exports.postgresQuery = function (connectionString, query) {
|
||||
|
||||
const client = new Client({ connectionString });
|
||||
|
||||
client.connect();
|
||||
|
||||
return client.query(query)
|
||||
.then(res => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(err => {
|
||||
client.connect((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
client.end();
|
||||
} else {
|
||||
// Connected here
|
||||
client.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
client.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
@ -317,6 +325,23 @@ exports.mysqlQuery = function (connectionString, query) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to and Ping a MongoDB database
|
||||
* @param {string} connectionString The database connection string
|
||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||
*/
|
||||
exports.mongodbPing = async function (connectionString) {
|
||||
let client = await MongoClient.connect(connectionString);
|
||||
let dbPing = await client.db().command({ ping: 1 });
|
||||
await client.close();
|
||||
|
||||
if (dbPing["ok"] === 1) {
|
||||
return "UP";
|
||||
} else {
|
||||
throw Error("failed");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Query radius server
|
||||
* @param {string} hostname Hostname of radius server
|
||||
@ -354,6 +379,30 @@ exports.radius = function (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redis server ping
|
||||
* @param {string} dsn The redis connection string
|
||||
*/
|
||||
exports.redisPingAsync = function (dsn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn,
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
client.connect().then(() => {
|
||||
client.ping().then((res, err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve value of setting based on key
|
||||
* @param {string} key Key of setting to retrieve
|
||||
|
@ -3,3 +3,7 @@ html[lang='fa'] {
|
||||
font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
}
|
||||
}
|
||||
|
||||
ul.multiselect__content {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export default {
|
||||
emits: [ "added" ],
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
modal: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
connectionTypes: [ "socket", "tcp" ],
|
||||
@ -91,11 +91,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
|
||||
/** Confirm deletion of docker host */
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show specified docker host
|
||||
* @param {number} dockerHostID
|
||||
*/
|
||||
show(dockerHostID) {
|
||||
if (dockerHostID) {
|
||||
let found = false;
|
||||
@ -126,6 +131,7 @@ export default {
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
/** Add docker host */
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
|
||||
@ -144,6 +150,7 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/** Test the docker host */
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
|
||||
@ -152,6 +159,7 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/** Delete this docker host */
|
||||
deleteDockerHost() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
|
||||
|
@ -41,7 +41,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
displayText() {
|
||||
if (this.item.value === "") {
|
||||
if (this.item.value === "" || this.item.value === undefined) {
|
||||
return this.item.name;
|
||||
} else {
|
||||
return `${this.item.name}: ${this.item.value}`;
|
||||
|
376
src/components/TagEditDialog.vue
Normal file
376
src/components/TagEditDialog.vue
Normal file
@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
{{ $t("Edit Tag") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="tag-name" class="form-label">{{ $t("Name") }}</label>
|
||||
<input id="tag-name" v-model="tag.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tag-color" class="form-label">{{ $t("Color") }}</label>
|
||||
<div class="d-flex">
|
||||
<div class="col-8 pe-1">
|
||||
<vue-multiselect
|
||||
v-model="selectedColor"
|
||||
:options="colorOptions"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="$t('color')"
|
||||
track-by="color"
|
||||
label="name"
|
||||
select-label=""
|
||||
deselect-label=""
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div
|
||||
class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #singleLabel="{ option }">
|
||||
<div
|
||||
class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</vue-multiselect>
|
||||
</div>
|
||||
<div class="col-4 ps-1">
|
||||
<input id="tag-color-hex" v-model="tag.color" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tag-monitors" class="form-label">{{ $tc("Monitor", selectedMonitors.length) }}</label>
|
||||
<div class="tag-monitors-list">
|
||||
<router-link v-for="monitor in selectedMonitors" :key="monitor.id" class="d-flex align-items-center justify-content-between text-decoration-none tag-monitors-list-row py-2 px-3" :to="monitorURL(monitor.id)" @click="modal.hide()">
|
||||
<span>{{ monitor.name }}</span>
|
||||
<button type="button" class="btn-rm-monitor btn btn-outline-danger ms-2 py-1" @click.stop.prevent="removeMonitor(monitor.id)">
|
||||
<font-awesome-icon class="" icon="times" />
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="allMonitorList.length > 0" class="pt-3 px-3">
|
||||
<label class="form-label">{{ $t("Add a monitor") }}:</label>
|
||||
<select v-model="selectedAddMonitor" class="form-control">
|
||||
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button v-if="tag" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteTag">
|
||||
{{ $t("confirmDeleteTagMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { colorOptions } from "../util-frontend";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueMultiselect,
|
||||
Confirm,
|
||||
},
|
||||
props: {
|
||||
updated: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: null,
|
||||
processing: false,
|
||||
selectedColor: {
|
||||
name: null,
|
||||
color: null,
|
||||
},
|
||||
tag: {
|
||||
id: null,
|
||||
name: "",
|
||||
color: "",
|
||||
// Do not set default value here, please scroll to show()
|
||||
},
|
||||
monitors: [],
|
||||
removingMonitor: [],
|
||||
addingMonitor: [],
|
||||
selectedAddMonitor: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
colorOptions() {
|
||||
if (!colorOptions(this).find(option => option.color === this.tag.color)) {
|
||||
return colorOptions(this).concat(
|
||||
{
|
||||
name: "custom",
|
||||
color: this.tag.color
|
||||
});
|
||||
} else {
|
||||
return colorOptions(this);
|
||||
}
|
||||
},
|
||||
selectedMonitors() {
|
||||
return this.monitors
|
||||
.concat(Object.values(this.$root.monitorList).filter(monitor => this.addingMonitor.includes(monitor.id)))
|
||||
.filter(monitor => !this.removingMonitor.includes(monitor.id));
|
||||
},
|
||||
allMonitorList() {
|
||||
return Object.values(this.$root.monitorList).filter(monitor => !this.selectedMonitors.includes(monitor));
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Set color option to "Custom" when a unknown color is entered
|
||||
"tag.color"(to, from) {
|
||||
if (colorOptions(this).find(x => x.color === to) == null) {
|
||||
this.selectedColor.name = this.$t("Custom");
|
||||
this.selectedColor.color = to;
|
||||
}
|
||||
},
|
||||
selectedColor(to, from) {
|
||||
if (to != null) {
|
||||
this.tag.color = to.color;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Selected a monitor and add to the list.
|
||||
*/
|
||||
selectedAddMonitor(monitor) {
|
||||
if (monitor) {
|
||||
if (this.removingMonitor.includes(monitor.id)) {
|
||||
this.removingMonitor = this.removingMonitor.filter(id => id !== monitor.id);
|
||||
} else {
|
||||
this.addingMonitor.push(monitor.id);
|
||||
}
|
||||
this.selectedAddMonitor = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show confirmation for deleting a tag
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load tag information for display in the edit dialog
|
||||
* @param {Object} tag tag object to edit
|
||||
* @returns {void}
|
||||
*/
|
||||
show(tag) {
|
||||
if (tag) {
|
||||
this.selectedColor = this.colorOptions.find(x => x.color === tag.color) ?? {
|
||||
name: this.$t("Custom"),
|
||||
color: tag.color
|
||||
};
|
||||
this.tag.id = tag.id;
|
||||
this.tag.name = tag.name;
|
||||
this.tag.color = tag.color;
|
||||
this.monitors = this.monitorsByTag(tag.id);
|
||||
this.removingMonitor = [];
|
||||
this.addingMonitor = [];
|
||||
this.selectedAddMonitor = null;
|
||||
}
|
||||
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit tag and monitorTag changes to server
|
||||
* @returns {void}
|
||||
*/
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
let editResult = true;
|
||||
|
||||
for (let addId of this.addingMonitor) {
|
||||
await this.addMonitorTagAsync(this.tag.id, addId, "").then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
editResult = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (let removeId of this.removingMonitor) {
|
||||
this.monitors.find(monitor => monitor.id === removeId)?.tags.forEach(async (monitorTag) => {
|
||||
await this.deleteMonitorTagAsync(this.tag.id, removeId, monitorTag.value).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
editResult = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("editTag", this.tag, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok && editResult) {
|
||||
this.updated();
|
||||
this.modal.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the editing tag from server
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteTag() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteTag", this.tag.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.updated();
|
||||
this.modal.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a monitor from the monitors list locally
|
||||
* @param {number} id id of the tag to remove
|
||||
* @returns {void}
|
||||
*/
|
||||
removeMonitor(id) {
|
||||
if (this.addingMonitor.includes(id)) {
|
||||
this.addingMonitor = this.addingMonitor.filter(x => x !== id);
|
||||
} else {
|
||||
this.removingMonitor.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get monitors which has a specific tag locally
|
||||
* @param {number} tagId id of the tag to filter
|
||||
* @returns {Object[]} list of monitors which has a specific tag
|
||||
*/
|
||||
monitorsByTag(tagId) {
|
||||
return Object.values(this.$root.monitorList).filter((monitor) => {
|
||||
return monitor.tags.find(monitorTag => monitorTag.tag_id === tagId);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get URL of monitor
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {string} Relative URL of monitor
|
||||
*/
|
||||
monitorURL(id) {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a tag to a monitor asynchronously
|
||||
* @param {number} tagId ID of tag to add
|
||||
* @param {number} monitorId ID of monitor to add tag to
|
||||
* @param {string} value Value of tag
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
addMonitorTagAsync(tagId, monitorId, value) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Delete a tag from a monitor asynchronously
|
||||
* @param {number} tagId ID of tag to remove
|
||||
* @param {number} monitorId ID of monitor to remove tag from
|
||||
* @param {string} value Value of tag
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
deleteMonitorTagAsync(tagId, monitorId, value) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-rm-monitor {
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
}
|
||||
|
||||
.tag-monitors-list {
|
||||
max-height: 40vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tag-monitors-list .tag-monitors-list-row {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
|
||||
.dark & {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
.dark &:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -130,6 +130,7 @@
|
||||
import { Modal } from "bootstrap";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { colorOptions } from "../util-frontend";
|
||||
import Tag from "../components/Tag.vue";
|
||||
const toast = useToast();
|
||||
|
||||
@ -176,24 +177,7 @@ export default {
|
||||
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id));
|
||||
},
|
||||
colorOptions() {
|
||||
return [
|
||||
{ name: this.$t("Gray"),
|
||||
color: "#4B5563" },
|
||||
{ name: this.$t("Red"),
|
||||
color: "#DC2626" },
|
||||
{ name: this.$t("Orange"),
|
||||
color: "#D97706" },
|
||||
{ name: this.$t("Green"),
|
||||
color: "#059669" },
|
||||
{ name: this.$t("Blue"),
|
||||
color: "#2563EB" },
|
||||
{ name: this.$t("Indigo"),
|
||||
color: "#4F46E5" },
|
||||
{ name: this.$t("Purple"),
|
||||
color: "#7C3AED" },
|
||||
{ name: this.$t("Pink"),
|
||||
color: "#DB2777" },
|
||||
];
|
||||
return colorOptions(this);
|
||||
},
|
||||
validateDraftTag() {
|
||||
let nameInvalid = false;
|
||||
@ -204,7 +188,7 @@ export default {
|
||||
nameInvalid = false;
|
||||
valueInvalid = false;
|
||||
invalid = false;
|
||||
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
|
||||
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0 && this.newDraftTag.select == null) {
|
||||
// Try to create new tag with existing name
|
||||
nameInvalid = true;
|
||||
invalid = true;
|
||||
|
@ -3,6 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
@ -24,7 +26,6 @@ export default {
|
||||
|
||||
computed: {
|
||||
uptime() {
|
||||
|
||||
if (this.type === "maintenance") {
|
||||
return this.$t("statusMaintenance");
|
||||
}
|
||||
@ -39,19 +40,19 @@ export default {
|
||||
},
|
||||
|
||||
color() {
|
||||
if (this.type === "maintenance" || this.monitor.maintenance) {
|
||||
if (this.lastHeartBeat.status === MAINTENANCE) {
|
||||
return "maintenance";
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 0) {
|
||||
if (this.lastHeartBeat.status === DOWN) {
|
||||
return "danger";
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 1) {
|
||||
if (this.lastHeartBeat.status === UP) {
|
||||
return "primary";
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 2) {
|
||||
if (this.lastHeartBeat.status === PENDING) {
|
||||
return "warning";
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,10 @@
|
||||
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
|
||||
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input id="promosms-allow-long" v-model="$parent.notification.promosmsAllowLongSMS" type="checkbox" class="form-check-input">
|
||||
<label for="promosms-allow-long" class="form-label">{{ $t("promosmsAllowLongSMS") }}</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
32
src/components/notifications/Splunk.vue
Normal file
32
src/components/notifications/Splunk.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="splunk-rest-url" class="form-label">{{ $t("Splunk Rest URL") }}</label>
|
||||
<HiddenInput id="splunk-rest-url" v-model="$parent.notification.splunkRestURL" :required="true" autocomplete="false"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="splunk-severity" class="form-label">{{ $t("Severity") }}</label>
|
||||
<select id="splunk-severity" v-model="$parent.notification.splunkSeverity" class="form-select">
|
||||
<option value="INFO">{{ $t("info") }}</option>
|
||||
<option value="WARNING">{{ $t("warning") }}</option>
|
||||
<option value="CRITICAL" selected="selected">{{ $t("critical") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="splunk-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
|
||||
<select id="splunk-resolve" v-model="$parent.notification.splunkAutoResolve" class="form-select">
|
||||
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
|
||||
<option value="ACKNOWLEDGEMENT">{{ $t("auto acknowledged") }}</option>
|
||||
<option value="RECOVERY">{{ $t("auto resolve") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -42,6 +42,11 @@ export default {
|
||||
HiddenInput,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Get the URL for telegram updates
|
||||
* @param {string} [mode=masked] Should the token be masked?
|
||||
* @returns {string} formatted URL
|
||||
*/
|
||||
telegramGetUpdatesURL(mode = "masked") {
|
||||
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
|
||||
|
||||
@ -55,6 +60,8 @@ export default {
|
||||
|
||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
|
||||
/** Get the telegram chat ID */
|
||||
async autoGetTelegramChatID() {
|
||||
try {
|
||||
let res = await axios.get(this.telegramGetUpdatesURL("withToken"));
|
||||
|
@ -44,6 +44,7 @@ import Webhook from "./Webhook.vue";
|
||||
import WeCom from "./WeCom.vue";
|
||||
import GoAlert from "./GoAlert.vue";
|
||||
import ZohoCliq from "./ZohoCliq.vue";
|
||||
import Splunk from "./Splunk.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
@ -92,6 +93,7 @@ const NotificationFormList = {
|
||||
"stackfield": Stackfield,
|
||||
"teams": Teams,
|
||||
"telegram": Telegram,
|
||||
"Splunk": Splunk,
|
||||
"webhook": Webhook,
|
||||
"WeCom": WeCom,
|
||||
"GoAlert": GoAlert,
|
||||
|
@ -7,6 +7,7 @@
|
||||
settings.keepDataPeriodDays,
|
||||
])
|
||||
}}
|
||||
{{ $t("infiniteRetention") }}
|
||||
</label>
|
||||
<input
|
||||
id="keepDataPeriodDays"
|
||||
@ -14,9 +15,12 @@
|
||||
type="number"
|
||||
class="form-control"
|
||||
required
|
||||
min="1"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
<div v-if="settings.keepDataPeriodDays < 0" class="form-text">
|
||||
{{ $t("dataRetentionTimeError") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||
|
@ -191,6 +191,7 @@ export default {
|
||||
location.reload();
|
||||
},
|
||||
|
||||
/** Show confirmation dialog for disable auth */
|
||||
confirmDisableAuth() {
|
||||
this.$refs.confirmDisableAuth.show();
|
||||
},
|
||||
|
171
src/components/settings/Tags.vue
Normal file
171
src/components/settings/Tags.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tags-list my-3">
|
||||
<div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)">
|
||||
<div class="col-5 ps-1">
|
||||
<Tag :item="tag" />
|
||||
</div>
|
||||
<div class="col-5 px-1">
|
||||
<div>{{ monitorsByTag(tag.id).length }} {{ $tc("Monitor", monitorsByTag(tag.id).length) }}</div>
|
||||
</div>
|
||||
<div class="col-2 pe-3 d-flex justify-content-end">
|
||||
<button type="button" class="btn ms-2 py-1">
|
||||
<font-awesome-icon class="" icon="edit" />
|
||||
</button>
|
||||
<button type="button" class="btn-rm-tag btn btn-outline-danger ms-2 py-1" :disabled="processing" @click.stop="deleteConfirm(index)">
|
||||
<font-awesome-icon class="" icon="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TagEditDialog ref="tagEditDialog" :updated="tagsUpdated" />
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteTag">
|
||||
{{ $t("confirmDeleteTagMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification";
|
||||
import TagEditDialog from "../../components/TagEditDialog.vue";
|
||||
import Tag from "../Tag.vue";
|
||||
import Confirm from "../Confirm.vue";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
TagEditDialog,
|
||||
Tag,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
tagsList: null,
|
||||
deletingTag: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getExistingTags();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Reflect tag changes in the UI by fetching data. Callback for the edit tag dialog.
|
||||
* @returns {void}
|
||||
*/
|
||||
tagsUpdated() {
|
||||
this.getExistingTags();
|
||||
this.$root.getMonitorList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of tags from server
|
||||
* @returns {void}
|
||||
*/
|
||||
getExistingTags() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("getTags", (res) => {
|
||||
this.processing = false;
|
||||
if (res.ok) {
|
||||
this.tagsList = res.tags;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show confirmation for deleting a tag
|
||||
* @param {number} index index of the tag to delete in the local tagsList
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteConfirm(index) {
|
||||
this.deletingTag = this.tagsList[index];
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show dialog for editing a tag
|
||||
* @param {number} index index of the tag to edit in the local tagsList
|
||||
* @returns {void}
|
||||
*/
|
||||
editTag(index) {
|
||||
this.$refs.tagEditDialog.show(this.tagsList[index]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the tag "deletingTag" from server
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteTag() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteTag", this.deletingTag.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.tagsUpdated();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get monitors which has a specific tag locally
|
||||
* @param {number} tagId id of the tag to filter
|
||||
* @returns {Object[]} list of monitors which has a specific tag
|
||||
*/
|
||||
monitorsByTag(tagId) {
|
||||
return Object.values(this.$root.monitorList).filter((monitor) => {
|
||||
return monitor.tags.find(monitorTag => monitorTag.tag_id === tagId);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.btn-rm-tag {
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
}
|
||||
|
||||
.tags-list .tags-list-row {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
|
||||
.dark & {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
.dark &:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-list .tags-list-row:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
</style>
|
@ -44,6 +44,7 @@ import {
|
||||
faWrench,
|
||||
faHeartbeat,
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
@ -88,6 +89,7 @@ library.add(
|
||||
faWrench,
|
||||
faHeartbeat,
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
@ -669,4 +669,10 @@ export default {
|
||||
"General Monitor Type": "Общ тип монитор",
|
||||
"Passive Monitor Type": "Пасивет тип монитор",
|
||||
"Specific Monitor Type": "Специфичен тип монитор",
|
||||
ZohoCliq: "ZohoCliq",
|
||||
wayToGetZohoCliqURL: "Можете да научите как се създава URL адрес за уеб кука {0}.",
|
||||
Kook: "Kook",
|
||||
wayToGetKookBotToken: "Създайте приложение и вземете вашия бот токен на {0}",
|
||||
wayToGetKookGuildID: "Превключете в 'Developer Mode' в 'Kook' настройките, след което десен клик върху 'guild' за да вземете неговото 'ID'",
|
||||
"Guild ID": "Guild ID",
|
||||
};
|
||||
|
@ -591,7 +591,7 @@ export default {
|
||||
"You can divide numbers with": "Čísla můžete dělit pomocí",
|
||||
"or": "nebo",
|
||||
recurringInterval: "Interval",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "Opakující se",
|
||||
strategyManual: "Aktivní/Neaktivní Ručně",
|
||||
warningTimezone: "Používá se časové pásmo serveru",
|
||||
weekdayShortMon: "Po",
|
||||
|
@ -74,6 +74,7 @@ export default {
|
||||
Current: "Current",
|
||||
Uptime: "Uptime",
|
||||
"Cert Exp.": "Cert Exp.",
|
||||
Monitor: "Monitor | Monitors",
|
||||
day: "day | days",
|
||||
"-day": "-day",
|
||||
hour: "hour",
|
||||
@ -190,6 +191,7 @@ export default {
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
Custom: "Custom",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
@ -320,6 +322,7 @@ export default {
|
||||
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
|
||||
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
|
||||
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||
promosmsAllowLongSMS: "Allow long SMS",
|
||||
"Feishu WebHookUrl": "Feishu WebHookURL",
|
||||
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
|
||||
"Internal Room Id": "Internal Room ID",
|
||||
@ -675,4 +678,7 @@ export default {
|
||||
"General Monitor Type": "General Monitor Type",
|
||||
"Passive Monitor Type": "Passive Monitor Type",
|
||||
"Specific Monitor Type": "Specific Monitor Type",
|
||||
dataRetentionTimeError: "Retention period must be 0 or greater",
|
||||
infiniteRetention: "Set to 0 for infinite retention.",
|
||||
confirmDeleteTagMsg: "Are you sure you want to delete this tag? Monitors associated with this tag will not be deleted.",
|
||||
};
|
||||
|
@ -209,6 +209,7 @@ export default {
|
||||
here: "ici",
|
||||
Required: "Requis",
|
||||
telegram: "Telegram",
|
||||
"ZohoCliq": "ZohoCliq",
|
||||
"Bot Token": "Jeton du robot",
|
||||
wayToGetTelegramToken: "Vous pouvez obtenir un token depuis {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
@ -240,7 +241,8 @@ export default {
|
||||
"Hello @everyone is...": "Bonjour {'@'}everyone il...",
|
||||
teams: "Microsoft Teams",
|
||||
"Webhook URL": "URL vers le webhook",
|
||||
wayToGetTeamsURL: "Vous pouvez apprendre comment créer un Webhook {0}.",
|
||||
wayToGetTeamsURL: "Vous pouvez apprendre comment créer une URL Webhook {0}.",
|
||||
wayToGetZohoCliqURL: "Vous pouvez apprendre comment créer une URL Webhook {0}.",
|
||||
signal: "Signal",
|
||||
Number: "Numéro",
|
||||
Recipients: "Destinataires",
|
||||
@ -270,6 +272,10 @@ export default {
|
||||
apprise: "Apprise (prend en charge plus de 50 services de notification)",
|
||||
GoogleChat: "Google Chat (Google Workspace uniquement)",
|
||||
pushbullet: "Pushbullet",
|
||||
Kook: "Kook",
|
||||
wayToGetKookBotToken: "Créez une application et récupérer le jeton de robot à l'addresse {0}",
|
||||
wayToGetKookGuildID: "Passez en « mode développeur » dans les paramètres de Kook, et cliquez droit sur le Guild pour obtenir son identifiant",
|
||||
"Guild ID": "Identifiant de Guild",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"User Key": "Clé d'utilisateur",
|
||||
@ -669,4 +675,6 @@ export default {
|
||||
"General Monitor Type": "Type de sonde générale",
|
||||
"Passive Monitor Type": "Type de sonde passive",
|
||||
"Specific Monitor Type": "Type de sonde spécifique",
|
||||
dataRetentionTimeError: "La durée de conservation doit être supérieure ou égale à 0",
|
||||
infiniteRetention: "Définissez la valeur à 0 pour une durée de conservation infinie.",
|
||||
};
|
||||
|
@ -284,6 +284,7 @@ export default {
|
||||
promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL",
|
||||
promosmsPhoneNumber: "Numer odbiorcy",
|
||||
promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)",
|
||||
promosmsAllowLongSMS: "Zezwól na długie SMSy",
|
||||
"Primary Base URL": "Główny URL",
|
||||
"Push URL": "Push URL",
|
||||
needPushEvery: "Powinieneś wywoływać ten URL co {0} sekund",
|
||||
|
@ -582,4 +582,97 @@ export default {
|
||||
goAlert: "GoAlert",
|
||||
backupOutdatedWarning: "Kullanımdan Kaldırıldı: Birçok özellik eklendiğinden ve bu yedekleme özelliği biraz bakımsız olduğundan, tam bir yedekleme oluşturamaz veya geri yükleyemez.",
|
||||
backupRecommend: "Lütfen bunun yerine birimi veya veri klasörünü (./data/) doğrudan yedekleyin.",
|
||||
enableGRPCTls: "TLS bağlantısıyla gRPC isteği göndermeye izin ver",
|
||||
grpcMethodDescription: "Yöntem adı, sayHello, check, vb. gibi cammelCase biçimine dönüştürülür.",
|
||||
Maintenance: "Bakım",
|
||||
statusMaintenance: "Bakım",
|
||||
"Schedule maintenance": "Bakım Planla",
|
||||
"Affected Monitors": "Etkilenen Monitörler",
|
||||
"Pick Affected Monitors...": "Etkilenen Monitörleri Seçin...",
|
||||
"Start of maintenance": "Bakım başlangıcı",
|
||||
"All Status Pages": "Tüm Durum Sayfaları",
|
||||
"Select status pages...": "Durum sayfalarını seçin...",
|
||||
recurringIntervalMessage: "Her gün bir kez çalıştırın | {0} günde bir çalıştırın",
|
||||
affectedMonitorsDescription: "Geçerli bakımdan etkilenen monitörleri seçin",
|
||||
affectedStatusPages: "Bu bakım mesajını seçili durum sayfalarında göster",
|
||||
atLeastOneMonitor: "Etkilenen en az bir monitör seçin",
|
||||
deleteMaintenanceMsg: "Bu bakımı silmek istediğinizden emin misiniz?",
|
||||
ZohoCliq: "ZohoCliq",
|
||||
webhookAdditionalHeadersTitle: "Ek Başlıklar",
|
||||
webhookAdditionalHeadersDesc: "Webhook ile gönderilen ek başlıkları ayarlar.",
|
||||
wayToGetZohoCliqURL: "Bir webhook URL'sinin nasıl oluşturulacağını öğrenebilirsiniz {0}.",
|
||||
Kook: "Kook",
|
||||
wayToGetKookBotToken: "Uygulama oluşturun ve {0} adresinde bot tokenı alın",
|
||||
wayToGetKookGuildID: "Kook ayarında \"Geliştirici Modu\"nu açın ve kimliğini almak için guild'e sağ tıklayın",
|
||||
"Guild ID": "Guild ID",
|
||||
smseagle: "SMSEagle",
|
||||
smseagleTo: "Telefon numara(ları)",
|
||||
smseagleGroup: "Telefon defteri grubu ad(lar)ı",
|
||||
smseagleContact: "Telefon rehberi kişi ad(lar)ı",
|
||||
smseagleRecipientType: "Alıcı Türü",
|
||||
smseagleRecipient: "Alıcı(lar) (birden çok olanlar virgülle ayrılmalıdır)",
|
||||
smseagleToken: "API Erişim Tokenı",
|
||||
smseagleUrl: "SMSEagle cihaz URL\"niz",
|
||||
smseagleEncoding: "Unicode olarak gönder",
|
||||
smseaglePriority: "Mesaj önceliği (0-9, varsayılan = 0)",
|
||||
Optional: "İsteğe bağlı",
|
||||
squadcast: "Squadcast",
|
||||
SendKey: "SendKey",
|
||||
"SMSManager API Docs": "SMSManager API Dökümanları ",
|
||||
"Gateway Type": "Ağ Geçidi Türü",
|
||||
SMSManager: "SMSManager",
|
||||
"You can divide numbers with": "Sayıları aşağıdakilerle bölebilirsiniz",
|
||||
or: "veya",
|
||||
recurringInterval: "Sıklık",
|
||||
Recurring: "Yineleme",
|
||||
strategyManual: "Manuel olarak Aktif/Pasif",
|
||||
warningTimezone: "Sunucunun kullandığı saat dilimi",
|
||||
weekdayShortMon: "Pzt",
|
||||
weekdayShortTue: "Sal",
|
||||
weekdayShortWed: "Çar",
|
||||
weekdayShortThu: "Per",
|
||||
weekdayShortFri: "Cum",
|
||||
weekdayShortSat: "Cmt",
|
||||
weekdayShortSun: "Paz",
|
||||
dayOfWeek: "Haftanın Günleri",
|
||||
dayOfMonth: "Ayın Günleri",
|
||||
lastDay: "Son Gün",
|
||||
lastDay1: "Ayın Son Günü",
|
||||
lastDay2: "Ayın 2. Son Günü",
|
||||
lastDay3: "Ayın 3. Son Günü",
|
||||
lastDay4: "Ayın 4. Son Günü",
|
||||
"No Maintenance": "Bakım Yok",
|
||||
pauseMaintenanceMsg: "Duraklatmak istediğinizden emin misiniz?",
|
||||
"maintenanceStatus-under-maintenance": "Bakımda",
|
||||
"maintenanceStatus-inactive": "Etkin Değil",
|
||||
"maintenanceStatus-scheduled": "Planlanmış",
|
||||
"maintenanceStatus-ended": "Bitti",
|
||||
"maintenanceStatus-unknown": "Bilinmiyor",
|
||||
"Display Timezone": "Saat dilimini göster",
|
||||
"Server Timezone": "Sunucu Saat Dilimi",
|
||||
statusPageMaintenanceEndDate: "Bitiş Zamanı",
|
||||
IconUrl: "Icon URL",
|
||||
"Enable DNS Cache": "DNS Önbelleğini Etkinleştir",
|
||||
Enable: "Etkin",
|
||||
Disable: "Devre Dışı",
|
||||
dnsCacheDescription: "Bazı IPv6 ortamlarında çalışmıyor olabilir, herhangi bir sorunla karşılaşırsanız devre dışı bırakın.",
|
||||
"Single Maintenance Window": "Tek Seferlik Bakım",
|
||||
"Maintenance Time Window of a Day": "Bür Günlük Bakım",
|
||||
"Effective Date Range": "Bakim Tarih Aralığı",
|
||||
"Schedule Maintenance": "Bakım Planla",
|
||||
"Date and Time": "Tarih ve Saat",
|
||||
"DateTime Range": "Tarih ve Saat Aralığı",
|
||||
Strategy: "Strateji",
|
||||
"Free Mobile User Identifier": "Ücretsiz Mobil Kullanıcı ID",
|
||||
"Free Mobile API Key": "Ücretsiz Mobil API Anahtarı",
|
||||
"Enable TLS": "TLS'yi Etkinleştir",
|
||||
"Proto Service Name": "Proto Service İsmi",
|
||||
"Proto Method": "Proto Method",
|
||||
"Proto Content": "Proto İçeriği",
|
||||
Economy: "Ekonomik",
|
||||
Lowcost: "Düşük maliyetli",
|
||||
high: "Yüksek",
|
||||
"General Monitor Type": "Genel Monitör Tipi",
|
||||
"Passive Monitor Type": "Pasif Monitör Tipi",
|
||||
"Specific Monitor Type": "Özel Monitör Tipi",
|
||||
};
|
||||
|
@ -2,17 +2,35 @@ export default {
|
||||
languageName: "简体中文",
|
||||
checkEverySecond: "检测频率 {0} 秒",
|
||||
retryCheckEverySecond: "重试间隔 {0} 秒",
|
||||
resendEveryXTimes: "每 {0} 次失败则重复发送一次",
|
||||
resendDisabled: "为 0 时禁用重复发送",
|
||||
retriesDescription: "服务被标记为故障并发送通知之前的最大重试次数",
|
||||
ignoreTLSError: "忽略 HTTPS 站点的 TLS/SSL 错误",
|
||||
upsideDownModeDescription: "反转状态监控,如果服务可访问,则认为是故障。",
|
||||
maxRedirectDescription: "允许的最大重定向次数。设置为 0 禁用重定向。",
|
||||
enableGRPCTls: "允许通过 TLS 连接发送 gRPC 请求",
|
||||
grpcMethodDescription: "方法名会转换为小驼峰格式,例如 sayHello、check 等等",
|
||||
acceptedStatusCodesDescription: "选择被视为成功响应的状态码。",
|
||||
Maintenance: "维护",
|
||||
statusMaintenance: "维护",
|
||||
"Schedule maintenance": "计划维护",
|
||||
"Affected Monitors": "受影响的监控项",
|
||||
"Pick Affected Monitors...": "选择受影响的监控项…",
|
||||
"Start of maintenance": "维护开始",
|
||||
"All Status Pages": "所有状态页面",
|
||||
"Select status pages...": "选择状态页面…",
|
||||
recurringIntervalMessage: "每天一次 | 每 {0} 天一次",
|
||||
affectedMonitorsDescription: "选择受当前维护影响的监控项",
|
||||
affectedStatusPages: "在所选状态页面上显示此维护消息",
|
||||
atLeastOneMonitor: "至少选择一个受影响的监控项",
|
||||
passwordNotMatchMsg: "两次输入的密码不一致。",
|
||||
notificationDescription: "通知必须被分配给监控项才能正常工作。",
|
||||
keywordDescription: "在纯 HTML 或 JSON 响应中搜索关键字,区分大小写。",
|
||||
pauseDashboardHome: "暂停",
|
||||
deleteMonitorMsg: "确定要删除此监控项吗?",
|
||||
deleteMaintenanceMsg: "确定要删除此维护吗?",
|
||||
deleteNotificationMsg: "确定要为所有监控项删除此通知吗?",
|
||||
dnsPortDescription: "DNS 服务器端口,默认为 53,您可以在任何时候更改此端口.",
|
||||
resolverserverDescription: "默认服务器是 Cloudflare。您随时可以修改解析服务器。",
|
||||
rrtypeDescription: "选择要监控的资源记录类型",
|
||||
pauseMonitorMsg: "确定要暂停吗?",
|
||||
@ -34,7 +52,6 @@ export default {
|
||||
Theme: "主题",
|
||||
General: "常规",
|
||||
"Primary Base URL": "站点主 URL",
|
||||
About: "关于",
|
||||
Version: "版本",
|
||||
"Check Update On GitHub": "检查 GitHub 上的更新",
|
||||
List: "列表",
|
||||
@ -72,6 +89,7 @@ export default {
|
||||
"Heartbeat Interval": "心跳间隔",
|
||||
Retries: "重试次数",
|
||||
"Heartbeat Retry Interval": "心跳重试间隔",
|
||||
"Resend Notification if Down X times consequently": "连续失败时重复发送通知的间隔次数",
|
||||
Advanced: "高级",
|
||||
"Upside Down Mode": "反转监控",
|
||||
"Max. Redirects": "最大重定向次数",
|
||||
@ -125,7 +143,6 @@ export default {
|
||||
"Last Result": "上次结果",
|
||||
"Create your admin account": "创建管理员账户",
|
||||
"Repeat Password": "重复密码",
|
||||
Backup: "备份",
|
||||
"Import Backup": "导入备份",
|
||||
"Export Backup": "导出备份",
|
||||
Export: "导出",
|
||||
@ -160,7 +177,7 @@ export default {
|
||||
Token: "令牌",
|
||||
"Show URI": "显示 URI",
|
||||
Tags: "标签",
|
||||
"Add New below or Select...": "在下面添加或选择...",
|
||||
"Add New below or Select...": "在下面添加或选择…",
|
||||
"Tag with this name already exist.": "相同名称的标签已存在。",
|
||||
"Tag with this value already exist.": "相同内容的标签已存在。",
|
||||
color: "颜色",
|
||||
@ -173,7 +190,7 @@ export default {
|
||||
Indigo: "靛蓝",
|
||||
Purple: "紫色",
|
||||
Pink: "粉色",
|
||||
"Search...": "搜索...",
|
||||
"Search...": "搜索…",
|
||||
"Avg. Ping": "平均 Ping",
|
||||
"Avg. Response": "平均响应",
|
||||
"Entry Page": "入口页面",
|
||||
@ -192,6 +209,7 @@ export default {
|
||||
here: "这里",
|
||||
Required: "必填",
|
||||
telegram: "Telegram",
|
||||
"ZohoCliq": "ZohoCliq",
|
||||
"Bot Token": "Bot Token",
|
||||
wayToGetTelegramToken: "您可以从 {0} 获取 Token。",
|
||||
"Chat ID": "Chat ID",
|
||||
@ -204,6 +222,8 @@ export default {
|
||||
"Content Type": "Content Type",
|
||||
webhookJsonDesc: "{0} 适合现代的 HTTP 服务器,例如 Express.js",
|
||||
webhookFormDataDesc: "{multipart} 适合 PHP,其中 JSON 需要使用 {decodeFunction} 解码",
|
||||
webhookAdditionalHeadersTitle: "额外 Header",
|
||||
webhookAdditionalHeadersDesc: "设置通过此 Webhook 发送的额外 Header。",
|
||||
smtp: "电子邮件(SMTP)",
|
||||
secureOptionNone: "无 / STARTTLS(常用端口 25、587)",
|
||||
secureOptionTLS: "TLS(常用端口 465)",
|
||||
@ -222,6 +242,7 @@ export default {
|
||||
teams: "Microsoft Teams",
|
||||
"Webhook URL": "Webhook URL",
|
||||
wayToGetTeamsURL: "您可以在{0}了解如何获取 Webhook URL。",
|
||||
wayToGetZohoCliqURL: "您可以在{0}了解如何创建 Webhook URL。",
|
||||
signal: "Signal",
|
||||
Number: "号码",
|
||||
Recipients: "收件人",
|
||||
@ -243,6 +264,7 @@ export default {
|
||||
"rocket.chat": "Rocket.Chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
PushByTechulus: "Push by Techulus",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
@ -250,9 +272,10 @@ export default {
|
||||
apprise: "Apprise (支持 50+ 种通知服务)",
|
||||
GoogleChat: "Google Chat(仅 Google Workspace)",
|
||||
pushbullet: "Pushbullet",
|
||||
AliyunSMS: "阿里云短信服务",
|
||||
Kook: "Kook",
|
||||
wayToGetKookBotToken: "在 {0} 创建应用并获取机器人 Token",
|
||||
wayToGetKookGuildID: "在Kook设置中打开 ‘开发者模式’,然后右键频道可获取其 ID",
|
||||
wayToGetKookGuildID: "在 Kook 设置中打开“开发者模式”,然后右键点击频道可获取其 ID",
|
||||
"Guild ID": "频道 ID",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
@ -298,6 +321,7 @@ export default {
|
||||
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(大约两倍 SMS FULL 的价格)。",
|
||||
promosmsPhoneNumber: "电话号码(波兰地区收信人可以不填区号)",
|
||||
promosmsSMSSender: "短信发信人名称:已注册的名称或以下默认值之一:InfoSMS、SMS Info、MaxSMS、INFO、SMS",
|
||||
Feishu: "飞书",
|
||||
"Feishu WebHookUrl": "飞书 WebHook URL",
|
||||
matrixHomeserverURL: "服务器 URL(包含 http(s):// 和可选的端口号)",
|
||||
"Internal Room Id": "内部房间 ID",
|
||||
@ -316,14 +340,18 @@ export default {
|
||||
"One record": "一条记录",
|
||||
steamApiKeyDescription: "要监控 Steam 游戏服务器,您需要 Steam Web-API 密钥。您可以在这里注册您的 API 密钥: ",
|
||||
"Current User": "当前用户",
|
||||
topic: "Topic",
|
||||
topicExplanation: "要监控的 MQTT Topic",
|
||||
successMessage: "成功消息",
|
||||
successMessageExplanation: "视为成功的 MQTT 消息",
|
||||
recent: "最近",
|
||||
Done: "完成",
|
||||
Info: "信息",
|
||||
Security: "安全性",
|
||||
"Steam API Key": "Steam API 密钥",
|
||||
"Shrink Database": "压缩数据库",
|
||||
"Pick a RR-Type...": "选择资源记录类型...",
|
||||
"Pick Accepted Status Codes...": "选择有效的状态码...",
|
||||
"Pick a RR-Type...": "选择资源记录类型…",
|
||||
"Pick Accepted Status Codes...": "选择有效的状态码…",
|
||||
Default: "默认",
|
||||
"HTTP Options": "HTTP 选项",
|
||||
"Create Incident": "创建事件",
|
||||
@ -333,6 +361,8 @@ export default {
|
||||
info: "信息",
|
||||
warning: "警告",
|
||||
danger: "危险",
|
||||
error: "错误",
|
||||
critical: "关键",
|
||||
primary: "主要",
|
||||
light: "明亮",
|
||||
dark: "黑暗",
|
||||
@ -360,7 +390,20 @@ export default {
|
||||
serwersmsAPIPassword: "API 密码",
|
||||
serwersmsPhoneNumber: "电话号码",
|
||||
serwersmsSenderName: "SMS 发信人名称(需要在客户中心注册)",
|
||||
smseagle: "SMSEagle",
|
||||
smseagleTo: "电话号码",
|
||||
smseagleGroup: "通讯录群组名",
|
||||
smseagleContact: "通讯录联系人",
|
||||
smseagleRecipientType: "收信人类型",
|
||||
smseagleRecipient: "收信人(多个需用半角逗号分隔)",
|
||||
smseagleToken: "API Access token",
|
||||
smseagleUrl: "您的 SMSEagle 设备 URL",
|
||||
smseagleEncoding: "以 Unicode 发送",
|
||||
smseaglePriority: "消息优先级(0-9,默认为 0)",
|
||||
stackfield: "Stackfield",
|
||||
Customize: "自定义",
|
||||
"Custom Footer": "自定义底部",
|
||||
"Custom CSS": "自定义 CSS",
|
||||
smtpDkimSettings: "DKIM 设置",
|
||||
smtpDkimDesc: "请访问 Nodemailer DKIM {0} 了解配置方法。",
|
||||
documentation: "文档",
|
||||
@ -370,16 +413,13 @@ export default {
|
||||
smtpDkimHashAlgo: "哈希算法(可选)",
|
||||
smtpDkimheaderFieldNames: "包含在哈希计算对象内的 Header 列表(可选)",
|
||||
smtpDkimskipFields: "不包含在哈希计算对象内的 Header 列表(可选)",
|
||||
Feishu: "飞书",
|
||||
AliyunSMS: "阿里云短信服务",
|
||||
"Sms template must contain parameters: ": "短信模板必须包含以下变量:",
|
||||
DingDing: "钉钉自定义机器人",
|
||||
WebHookUrl: "钉钉自定义机器人 Webhook 地址",
|
||||
SecretKey: "钉钉自定义机器人加签密钥",
|
||||
"For safety, must use secret key": "出于安全考虑,必须使用加签密钥",
|
||||
WeCom: "企业微信群机器人",
|
||||
"WeCom Bot Key": "企业微信群机器人 Key",
|
||||
PushByTechulus: "Push by Techulus",
|
||||
wayToGetPagerDutyKey: "您可以在 Service -> Service Directory -> (选择一个 Service) -> Integrations -> Add integration 页面中搜索“Events API V2”以获取此 Integration Key,更多信息请看{0}",
|
||||
"Integration Key": "Integration Key",
|
||||
"Integration URL": "Integration URL",
|
||||
"Auto resolve or acknowledged": "自动标记为已解决或已读",
|
||||
"do nothing": "不做任何操作",
|
||||
"auto acknowledged": "自动标记为已读",
|
||||
"auto resolve": "自动标记为已解决",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "API 接入点",
|
||||
@ -396,9 +436,6 @@ export default {
|
||||
proxyDescription: "代理必须配置到至少一个监控项后才会工作。",
|
||||
enableProxyDescription: "此代理必须启用才能对监控项的网络请求起作用。您可以通过修改激活状态,临时在所有监控项中禁用此代理。",
|
||||
setAsDefaultProxyDescription: "此代理会对新创建的监控项默认激活,您仍可以在监控项配置中单独禁用此代理。",
|
||||
"Proxy Protocol": "代理协议",
|
||||
"Proxy Server": "代理服务器",
|
||||
"Server Address": "服务器地址",
|
||||
"Certificate Chain": "证书链",
|
||||
Valid: "有效",
|
||||
Invalid: "无效",
|
||||
@ -407,9 +444,14 @@ export default {
|
||||
PhoneNumbers: "PhoneNumbers",
|
||||
TemplateCode: "TemplateCode",
|
||||
SignName: "SignName",
|
||||
"Sms template must contain parameters: ": "短信模板必须包含以下变量:",
|
||||
"Bark Endpoint": "Bark 接入点",
|
||||
"Bark Group": "Bark 群组",
|
||||
"Bark Sound": "Bark 铃声",
|
||||
DingDing: "钉钉自定义机器人",
|
||||
WebHookUrl: "钉钉自定义机器人 Webhook 地址",
|
||||
SecretKey: "钉钉自定义机器人加签密钥",
|
||||
"For safety, must use secret key": "出于安全考虑,必须使用加签密钥",
|
||||
"Device Token": "Apple Device Token",
|
||||
Platform: "平台",
|
||||
iOS: "iOS",
|
||||
@ -418,21 +460,18 @@ export default {
|
||||
High: "高",
|
||||
Retry: "重试次数",
|
||||
Topic: "Gorush Topic",
|
||||
WeCom: "企业微信群机器人",
|
||||
"WeCom Bot Key": "企业微信群机器人 Key",
|
||||
"Setup Proxy": "设置代理",
|
||||
"Proxy Protocol": "代理协议",
|
||||
"Proxy Server": "代理服务器",
|
||||
"Server Address": "服务器地址",
|
||||
"Proxy server has authentication": "代理服务器启用了身份验证功能",
|
||||
User: "用户名",
|
||||
Installed: "已安装",
|
||||
"Not installed": "未安装",
|
||||
Running: "运行中",
|
||||
"Not running": "未运行",
|
||||
"Message:": "信息:",
|
||||
wayToGetCloudflaredURL: "(可从 {0} 下载 cloudflared)",
|
||||
cloudflareWebsite: "Cloudflare 网站",
|
||||
"Don't know how to get the token? Please read the guide:": "不知道如何获取 Token?请阅读指南:",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您正在通过 Cloudflare Tunnel 访问网站,则停止可能会导致当前连接断开。您确定要停止吗?请输入密码以确认。",
|
||||
"Other Software": "其他软件",
|
||||
"For example: nginx, Apache and Traefik.": "例如:nginx、Apache 和 Traefik。",
|
||||
"Please read": "请阅读",
|
||||
"Remove Token": "移除 Token",
|
||||
Start: "启动",
|
||||
Stop: "停止",
|
||||
@ -450,6 +489,18 @@ export default {
|
||||
"New Status Page": "新的状态页",
|
||||
"Page Not Found": "未找到该页面",
|
||||
"Reverse Proxy": "反向代理",
|
||||
Backup: "备份",
|
||||
About: "关于",
|
||||
wayToGetCloudflaredURL: "(可从 {0} 下载 cloudflared)",
|
||||
cloudflareWebsite: "Cloudflare 网站",
|
||||
"Message:": "信息:",
|
||||
"Don't know how to get the token? Please read the guide:": "不知道如何获取 Token?请阅读指南:",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您正在通过 Cloudflare Tunnel 访问网站,则停止可能会导致当前连接断开。您确定要停止吗?请输入密码以确认。",
|
||||
"HTTP Headers": "HTTP 头",
|
||||
"Trust Proxy": "可信的代理类字段",
|
||||
"Other Software": "其他软件",
|
||||
"For example: nginx, Apache and Traefik.": "例如:nginx、Apache 和 Traefik。",
|
||||
"Please read": "请阅读",
|
||||
"Subject:": "颁发给:",
|
||||
"Valid To:": "有效期至:",
|
||||
"Days Remaining:": "剩余有效天数:",
|
||||
@ -459,26 +510,28 @@ export default {
|
||||
"Domain Name Expiry Notification": "域名到期时通知",
|
||||
Proxy: "代理",
|
||||
"Date Created": "创建于",
|
||||
HomeAssistant: "Home Assistant",
|
||||
onebotHttpAddress: "OneBot HTTP 地址",
|
||||
onebotMessageType: "OneBot 消息类型",
|
||||
onebotGroupMessage: "群聊",
|
||||
onebotPrivateMessage: "私聊",
|
||||
onebotUserOrGroupId: "群组/用户 ID",
|
||||
onebotSafetyTips: "出于安全原因,请务必设置 AccessToken",
|
||||
topic: "Topic",
|
||||
topicExplanation: "MQTT 传递给监控的 Topic",
|
||||
successMessage: "成功时消息",
|
||||
successMessageExplanation: "MQTT 成功时所传递的消息",
|
||||
Customize: "自定义",
|
||||
"Custom Footer": "自定义底部",
|
||||
"Custom CSS": "自定义 CSS",
|
||||
"PushDeer Key": "PushDeer Key",
|
||||
"Footer Text": "底部自定义文本",
|
||||
"Show Powered By": "显示 Powered By",
|
||||
"Domain Names": "域名",
|
||||
signedInDisp: "当前用户: {0}",
|
||||
signedInDispDisabled: "已禁用身份验证",
|
||||
RadiusSecret: "Radius 共享机密",
|
||||
RadiusSecretDescription: "客户端和服务器之间共享的密钥",
|
||||
RadiusCalledStationId: "NAS 网络访问服务器号码(Called Station Id)",
|
||||
RadiusCalledStationIdDescription: "所访问的服务器的标识",
|
||||
RadiusCallingStationId: "呼叫方号码(Calling Station Id)",
|
||||
RadiusCallingStationIdDescription: "发出请求的设备的标识",
|
||||
"Certificate Expiry Notification": "证书到期时通知",
|
||||
"API Username": "API 凭证 Username",
|
||||
"API Key": "API 凭证 Key",
|
||||
"API Username": "API Username",
|
||||
"API Key": "API Key",
|
||||
"Recipient Number": "收件人手机号码",
|
||||
"From Name/Number": "发件人名称/手机号码",
|
||||
"Leave blank to use a shared sender number.": "留空以使用平台共享的发件人手机号码",
|
||||
@ -526,38 +579,11 @@ export default {
|
||||
"Retype the address.": "重新输入地址;",
|
||||
"Go back to the previous page.": "返回到上一页面。",
|
||||
"Coming Soon": "即将推出",
|
||||
wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。",
|
||||
signedInDisp: "当前用户: {0}",
|
||||
signedInDispDisabled: "已禁用身份验证",
|
||||
dnsPortDescription: "DNS 服务器端口,默认为 53,你可以在任何时候更改此端口.",
|
||||
error: "错误",
|
||||
critical: "关键",
|
||||
wayToGetPagerDutyKey: "你可以在 Service -> Service Directory -> (选择一个 Service) -> Integrations -> Add integration 页面中搜索 \"Events API V2\" 以获取此 Integration Key,更多信息请参见 {0}",
|
||||
"Integration Key": "Integration Key",
|
||||
"Integration URL": "Integration URL",
|
||||
"Auto resolve or acknowledged": "自动标记为已解决或已读",
|
||||
"do nothing": "不做任何操作",
|
||||
"auto acknowledged": "自动标记为已读",
|
||||
"auto resolve": "自动标记为已解决",
|
||||
wayToGetClickSendSMSToken: "您可以在{0}获取 API Username 和 API Key。",
|
||||
"Connection String": "连接字符串",
|
||||
Query: "查询语句",
|
||||
settingsCertificateExpiry: "TLS 证书过期通知",
|
||||
certificationExpiryDescription: "HTTPS 监控项发现被监控目标的 TLS 证书剩余有效期少于以下天数时将发出通知:",
|
||||
"ntfy Topic": "ntfy 主题",
|
||||
Domain: "域名",
|
||||
Workstation: "工作站",
|
||||
resendEveryXTimes: "每 {0} 次失败则重复发送一次",
|
||||
resendDisabled: "为 0 时禁用重复发送",
|
||||
"Resend Notification if Down X times consequently": "连续失败时重复发送通知的间隔次数",
|
||||
"HTTP Headers": "HTTP 头",
|
||||
"Trust Proxy": "可信的代理类字段",
|
||||
HomeAssistant: "Home Assistant",
|
||||
RadiusSecret: "Radius 共享机密",
|
||||
RadiusSecretDescription: "客户端和服务器之间共享的密钥",
|
||||
RadiusCalledStationId: "NAS 网络访问服务器号码(Called Station Id)",
|
||||
RadiusCalledStationIdDescription: "所访问的服务器的标识",
|
||||
RadiusCallingStationId: "呼叫方号码(Calling Station Id)",
|
||||
RadiusCallingStationIdDescription: "发出请求的设备的标识",
|
||||
"Setup Docker Host": "配置 Docker 宿主信息",
|
||||
"Connection Type": "连接方式",
|
||||
"Docker Daemon": "Docker 守护进程",
|
||||
@ -568,6 +594,9 @@ export default {
|
||||
"Container Name / ID": "容器名称 / ID",
|
||||
"Docker Host": "Docker 宿主",
|
||||
"Docker Hosts": "Docker 宿主",
|
||||
"ntfy Topic": "ntfy Topic",
|
||||
Domain: "域名",
|
||||
Workstation: "工作站",
|
||||
disableCloudflaredNoAuthMsg: "您现在正处于 No Auth 模式,无需输入密码",
|
||||
trustProxyDescription: "信任 'X-Forwarded-*' 头。如果您的 Uptime Kuma 是通过 Nginx 或 Apache 等反代服务对外提供访问的话,则您应当启用本功能以获取正确的客户端 IP。",
|
||||
wayToGetLineNotifyToken: "您可以在 {0} 获取 Access token",
|
||||
@ -584,7 +613,7 @@ export default {
|
||||
"Event data:": "事件数据:",
|
||||
"Then choose an action, for example switch the scene to where an RGB light is red.": "然后您可以选择关联操作,例如切换到 RGB 灯发出红光的场景",
|
||||
"Frontend Version": "前端版本",
|
||||
"Frontend Version do not match backend version!": "前端版本与后端版本不符!",
|
||||
"Frontend Version do not match backend version!": "前端版本与后端版本不匹配!",
|
||||
"Base URL": "API 基础地址",
|
||||
goAlertInfo: "GoAlert 是一个用于呼叫调度、自动汇报和通知(如 SMS 或语音呼叫)的开源应用程序。在正确的时间以正确的方式自动让正确的人参与!{0}",
|
||||
goAlertIntegrationKeyInfo: "使用形如 aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee 的通用 API 集成密钥,通常是复制来的链接中的 token 参数值。",
|
||||
@ -598,5 +627,57 @@ export default {
|
||||
"Gateway Type": "网关类型",
|
||||
SMSManager: "SMSManager",
|
||||
"You can divide numbers with": "可用的分隔符:",
|
||||
"or": "或",
|
||||
or: "或",
|
||||
recurringInterval: "时间间隔",
|
||||
Recurring: "重复",
|
||||
strategyManual: "手动启用/禁用",
|
||||
warningTimezone: "使用服务器时区",
|
||||
weekdayShortMon: "周一",
|
||||
weekdayShortTue: "周二",
|
||||
weekdayShortWed: "周三",
|
||||
weekdayShortThu: "周四",
|
||||
weekdayShortFri: "周五",
|
||||
weekdayShortSat: "周六",
|
||||
weekdayShortSun: "周日",
|
||||
dayOfWeek: "每周计划",
|
||||
dayOfMonth: "每月计划",
|
||||
lastDay: "结束日",
|
||||
lastDay1: "每月最后一天",
|
||||
lastDay2: "每月倒数第二天",
|
||||
lastDay3: "每月倒数第三天",
|
||||
lastDay4: "每月倒数第四天",
|
||||
"No Maintenance": "无维护计划",
|
||||
pauseMaintenanceMsg: "确定要暂停吗?",
|
||||
"maintenanceStatus-under-maintenance": "正在维护",
|
||||
"maintenanceStatus-inactive": "未启用",
|
||||
"maintenanceStatus-scheduled": "已计划",
|
||||
"maintenanceStatus-ended": "已结束",
|
||||
"maintenanceStatus-unknown": "未知",
|
||||
"Display Timezone": "显示时区",
|
||||
"Server Timezone": "服务器时区",
|
||||
statusPageMaintenanceEndDate: "结束时间",
|
||||
IconUrl: "图标 URL",
|
||||
"Enable DNS Cache": "启用 DNS 缓存",
|
||||
Enable: "启用",
|
||||
Disable: "禁用",
|
||||
dnsCacheDescription: "可能无法在某些 IPv6 环境工作,如果遇到问题请禁用。",
|
||||
"Single Maintenance Window": "单一时间窗口",
|
||||
"Maintenance Time Window of a Day": "每日维护时间窗口",
|
||||
"Effective Date Range": "生效日期范围",
|
||||
"Schedule Maintenance": "计划维护",
|
||||
"Date and Time": "日期时间",
|
||||
"DateTime Range": "日期时间范围",
|
||||
Strategy: "策略",
|
||||
"Free Mobile User Identifier": "Free Mobile User Identifier",
|
||||
"Free Mobile API Key": "Free Mobile API Key",
|
||||
"Enable TLS": "启用 TLS",
|
||||
"Proto Service Name": "Proto 服务名称",
|
||||
"Proto Method": "Proto 方法",
|
||||
"Proto Content": "Proto 内容",
|
||||
Economy: "经济",
|
||||
Lowcost: "低价",
|
||||
high: "高价",
|
||||
"General Monitor Type": "常规监控类型",
|
||||
"Passive Monitor Type": "被动监控类型",
|
||||
"Specific Monitor Type": "针对监控类型",
|
||||
};
|
||||
|
@ -63,6 +63,12 @@
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="https://github.com/louislam/uptime-kuma/wiki" class="dropdown-item" target="_blank">
|
||||
<font-awesome-icon icon="info-circle" /> {{ $t("Help") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
|
||||
<button class="dropdown-item" @click="$root.logout">
|
||||
<font-awesome-icon icon="sign-out-alt" />
|
||||
|
@ -12,6 +12,11 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Convert value to UTC
|
||||
* @param {string | number | Date | dayjs.Dayjs} value
|
||||
* @returns {dayjs.Dayjs}
|
||||
*/
|
||||
toUTC(value) {
|
||||
return dayjs.tz(value, this.timezone).utc().format();
|
||||
},
|
||||
@ -34,6 +39,11 @@ export default {
|
||||
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
|
||||
/**
|
||||
* Get time for maintenance
|
||||
* @param {string | number | Date | dayjs.Dayjs} value
|
||||
* @returns {string}
|
||||
*/
|
||||
datetimeMaintenance(value) {
|
||||
const inputDate = new Date(value);
|
||||
const now = new Date(Date.now());
|
||||
|
@ -3,6 +3,7 @@ import { useToast } from "vue-toastification";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import Favico from "favico.js";
|
||||
import dayjs from "dayjs";
|
||||
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
|
||||
const toast = useToast();
|
||||
|
||||
let socket;
|
||||
@ -454,6 +455,10 @@ export default {
|
||||
socket.emit("getMonitorList", callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of maintenances
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
getMaintenanceList(callback) {
|
||||
if (! callback) {
|
||||
callback = () => { };
|
||||
@ -470,22 +475,49 @@ export default {
|
||||
socket.emit("add", monitor, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a maintenace
|
||||
* @param {Object} maintenance
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
addMaintenance(maintenance, callback) {
|
||||
socket.emit("addMaintenance", maintenance, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add monitors to maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {number[]} monitors
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
addMonitorMaintenance(maintenanceID, monitors, callback) {
|
||||
socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add status page to maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {number} statusPages
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
addMaintenanceStatusPage(maintenanceID, statusPages, callback) {
|
||||
socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get monitors affected by maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
getMonitorMaintenance(maintenanceID, callback) {
|
||||
socket.emit("getMonitorMaintenance", maintenanceID, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get status pages where maintenance is shown
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
getMaintenanceStatusPage(maintenanceID, callback) {
|
||||
socket.emit("getMaintenanceStatusPage", maintenanceID, callback);
|
||||
},
|
||||
@ -499,6 +531,11 @@ export default {
|
||||
socket.emit("deleteMonitor", monitorID, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete specified maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
deleteMaintenance(maintenanceID, callback) {
|
||||
socket.emit("deleteMaintenance", maintenanceID, callback);
|
||||
},
|
||||
@ -590,28 +627,28 @@ export default {
|
||||
for (let monitorID in this.lastHeartbeatList) {
|
||||
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
||||
|
||||
if (this.monitorList[monitorID] && this.monitorList[monitorID].maintenance) {
|
||||
result[monitorID] = {
|
||||
text: this.$t("statusMaintenance"),
|
||||
color: "maintenance",
|
||||
};
|
||||
} else if (! lastHeartBeat) {
|
||||
if (! lastHeartBeat) {
|
||||
result[monitorID] = unknown;
|
||||
} else if (lastHeartBeat.status === 1) {
|
||||
} else if (lastHeartBeat.status === UP) {
|
||||
result[monitorID] = {
|
||||
text: this.$t("Up"),
|
||||
color: "primary",
|
||||
};
|
||||
} else if (lastHeartBeat.status === 0) {
|
||||
} else if (lastHeartBeat.status === DOWN) {
|
||||
result[monitorID] = {
|
||||
text: this.$t("Down"),
|
||||
color: "danger",
|
||||
};
|
||||
} else if (lastHeartBeat.status === 2) {
|
||||
} else if (lastHeartBeat.status === PENDING) {
|
||||
result[monitorID] = {
|
||||
text: this.$t("Pending"),
|
||||
color: "warning",
|
||||
};
|
||||
} else if (lastHeartBeat.status === MAINTENANCE) {
|
||||
result[monitorID] = {
|
||||
text: this.$t("statusMaintenance"),
|
||||
color: "maintenance",
|
||||
};
|
||||
} else {
|
||||
result[monitorID] = unknown;
|
||||
}
|
||||
@ -633,17 +670,17 @@ export default {
|
||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||
let monitor = this.$root.monitorList[monitorID];
|
||||
|
||||
if (monitor && monitor.maintenance) {
|
||||
result.maintenance++;
|
||||
} else if (monitor && ! monitor.active) {
|
||||
if (monitor && ! monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
if (beat.status === 1) {
|
||||
if (beat.status === UP) {
|
||||
result.up++;
|
||||
} else if (beat.status === 0) {
|
||||
} else if (beat.status === DOWN) {
|
||||
result.down++;
|
||||
} else if (beat.status === 2) {
|
||||
} else if (beat.status === PENDING) {
|
||||
result.up++;
|
||||
} else if (beat.status === MAINTENANCE) {
|
||||
result.maintenance++;
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
|
@ -473,6 +473,12 @@ table {
|
||||
|
||||
.dropdown-clear-data {
|
||||
float: right;
|
||||
|
||||
ul {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
@ -356,6 +356,7 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/** Initialise page */
|
||||
init() {
|
||||
this.affectedMonitors = [];
|
||||
this.selectedStatusPages = [];
|
||||
@ -414,6 +415,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/** Create new maintenance */
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
@ -458,6 +460,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add monitor to maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
async addMonitorMaintenance(maintenanceID, callback) {
|
||||
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
|
||||
if (!res.ok) {
|
||||
@ -470,6 +477,11 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add status page to maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
async addMaintenanceStatusPage(maintenanceID, callback) {
|
||||
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
|
||||
if (!res.ok) {
|
||||
|
@ -57,9 +57,15 @@
|
||||
<option value="mysql">
|
||||
MySQL/MariaDB
|
||||
</option>
|
||||
<option value="mongodb">
|
||||
MongoDB
|
||||
</option>
|
||||
<option value="radius">
|
||||
Radius
|
||||
</option>
|
||||
<option value="redis">
|
||||
Redis
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
@ -105,7 +111,7 @@
|
||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
@ -267,6 +273,24 @@
|
||||
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Redis -->
|
||||
<template v-if="monitor.type === 'redis'">
|
||||
<div class="my-3">
|
||||
<label for="redisConnectionString" class="form-label">{{ $t("Connection String") }}</label>
|
||||
<input id="redisConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="redis://user:password@host:port">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- MongoDB -->
|
||||
<template v-if="monitor.type === 'mongodb'">
|
||||
<div class="my-3">
|
||||
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
|
||||
|
||||
<template v-if="monitor.type === 'mongodb'">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="mongodb://username:password@host:port/database">
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Interval -->
|
||||
<div class="my-3">
|
||||
@ -576,6 +600,7 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
|
||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||
import TagsManager from "../components/TagsManager.vue";
|
||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
||||
import { hostNameRegexPattern } from "../util-frontend";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@ -600,11 +625,8 @@ export default {
|
||||
},
|
||||
acceptedStatusCodeOptions: [],
|
||||
dnsresolvetypeOptions: [],
|
||||
|
||||
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
|
||||
ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))",
|
||||
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
|
||||
hostnameRegexPattern: "^(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$"
|
||||
ipOrHostnameRegexPattern: hostNameRegexPattern(),
|
||||
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true)
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -65,6 +65,7 @@ export default {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
/** Initialise page */
|
||||
init() {
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
@ -83,10 +84,12 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/** Confirm deletion */
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/** Delete maintenance after showing confirmation */
|
||||
deleteMaintenance() {
|
||||
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
|
||||
if (res.ok) {
|
||||
|
@ -133,15 +133,25 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get maintenance URL
|
||||
* @param {number} id
|
||||
* @returns {string} Relative URL
|
||||
*/
|
||||
maintenanceURL(id) {
|
||||
return getMaintenanceRelativeURL(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show delete confirmation
|
||||
* @param {number} maintenanceID
|
||||
*/
|
||||
deleteDialog(maintenanceID) {
|
||||
this.selectedMaintenanceID = maintenanceID;
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/** Delete maintenance after showing confirmation dialog */
|
||||
deleteMaintenance() {
|
||||
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
|
||||
if (res.ok) {
|
||||
|
@ -95,6 +95,9 @@ export default {
|
||||
"reverse-proxy": {
|
||||
title: this.$t("Reverse Proxy"),
|
||||
},
|
||||
tags: {
|
||||
title: this.$t("Tags"),
|
||||
},
|
||||
"monitor-history": {
|
||||
title: this.$t("Monitor History"),
|
||||
},
|
||||
@ -189,6 +192,8 @@ export default {
|
||||
* @param {string} [currentPassword] Only need for disableAuth to true
|
||||
*/
|
||||
saveSettings(callback, currentPassword) {
|
||||
let valid = this.validateSettings();
|
||||
if (valid.success) {
|
||||
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.loadSettings();
|
||||
@ -197,6 +202,26 @@ export default {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$root.toastError(valid.msg);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure settings are valid
|
||||
* @returns {Object} Contains success state and error msg
|
||||
*/
|
||||
validateSettings() {
|
||||
if (this.settings.keepDataPeriodDays < 0) {
|
||||
return {
|
||||
success: false,
|
||||
msg: this.$t("dataRetentionTimeError"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
msg: "",
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ import Appearance from "./components/settings/Appearance.vue";
|
||||
import General from "./components/settings/General.vue";
|
||||
const Notifications = () => import("./components/settings/Notifications.vue");
|
||||
import ReverseProxy from "./components/settings/ReverseProxy.vue";
|
||||
import Tags from "./components/settings/Tags.vue";
|
||||
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||
const Security = () => import("./components/settings/Security.vue");
|
||||
import Proxies from "./components/settings/Proxies.vue";
|
||||
@ -95,6 +96,10 @@ const routes = [
|
||||
path: "reverse-proxy",
|
||||
component: ReverseProxy,
|
||||
},
|
||||
{
|
||||
path: "tags",
|
||||
component: Tags,
|
||||
},
|
||||
{
|
||||
path: "monitor-history",
|
||||
component: MonitorHistory,
|
||||
|
@ -78,3 +78,45 @@ export function getResBaseURL() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} mqtt wheather or not the regex should take into account the fact that it is an mqtt uri
|
||||
* @returns RegExp The requested regex
|
||||
*/
|
||||
export function hostNameRegexPattern(mqtt = false) {
|
||||
// mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect)
|
||||
const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?";
|
||||
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
|
||||
const ipRegexPattern = `((^\\s*${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))`;
|
||||
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
|
||||
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$`;
|
||||
|
||||
return `${ipRegexPattern}|${hostNameRegexPattern}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag color options
|
||||
* Shared between components
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function colorOptions(self) {
|
||||
return [
|
||||
{ name: self.$t("Gray"),
|
||||
color: "#4B5563" },
|
||||
{ name: self.$t("Red"),
|
||||
color: "#DC2626" },
|
||||
{ name: self.$t("Orange"),
|
||||
color: "#D97706" },
|
||||
{ name: self.$t("Green"),
|
||||
color: "#059669" },
|
||||
{ name: self.$t("Blue"),
|
||||
color: "#2563EB" },
|
||||
{ name: self.$t("Indigo"),
|
||||
color: "#4F46E5" },
|
||||
{ name: self.$t("Purple"),
|
||||
color: "#7C3AED" },
|
||||
{ name: self.$t("Pink"),
|
||||
color: "#DB2777" },
|
||||
];
|
||||
}
|
||||
|
16
src/util.js
16
src/util.js
@ -315,6 +315,11 @@ function getMonitorRelativeURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
}
|
||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
||||
/**
|
||||
* Get relative path for maintenance
|
||||
* @param id ID of maintenance
|
||||
* @returns Formatted relative path
|
||||
*/
|
||||
function getMaintenanceRelativeURL(id) {
|
||||
return "/maintenance/" + id;
|
||||
}
|
||||
@ -361,6 +366,11 @@ function parseTimeFromTimeObject(obj) {
|
||||
return result;
|
||||
}
|
||||
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
|
||||
/**
|
||||
* Convert ISO date to UTC
|
||||
* @param input Date
|
||||
* @returns ISO Date time
|
||||
*/
|
||||
function isoToUTCDateTime(input) {
|
||||
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
|
||||
}
|
||||
@ -379,6 +389,12 @@ function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||
return dayjs.utc(input).local().format(format);
|
||||
}
|
||||
exports.utcToLocal = utcToLocal;
|
||||
/**
|
||||
* Convert local datetime to UTC
|
||||
* @param input Local date
|
||||
* @param format Format to return
|
||||
* @returns Date in requested format
|
||||
*/
|
||||
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
|
||||
return dayjs(input).utc().format(format);
|
||||
}
|
||||
|
17
src/util.ts
17
src/util.ts
@ -352,6 +352,11 @@ export function getMonitorRelativeURL(id: string) {
|
||||
return "/dashboard/" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative path for maintenance
|
||||
* @param id ID of maintenance
|
||||
* @returns Formatted relative path
|
||||
*/
|
||||
export function getMaintenanceRelativeURL(id: string) {
|
||||
return "/maintenance/" + id;
|
||||
}
|
||||
@ -405,7 +410,11 @@ export function parseTimeFromTimeObject(obj : any) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert ISO date to UTC
|
||||
* @param input Date
|
||||
* @returns ISO Date time
|
||||
*/
|
||||
export function isoToUTCDateTime(input : string) {
|
||||
return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
|
||||
}
|
||||
@ -424,6 +433,12 @@ export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
|
||||
return dayjs.utc(input).local().format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert local datetime to UTC
|
||||
* @param input Local date
|
||||
* @param format Format to return
|
||||
* @returns Date in requested format
|
||||
*/
|
||||
export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
|
||||
return dayjs(input).utc().format(format);
|
||||
}
|
||||
|
44
test/cypress/unit/i18n.spec.js
Normal file
44
test/cypress/unit/i18n.spec.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { currentLocale } from "../../../src/i18n";
|
||||
|
||||
describe("Test i18n.js", () => {
|
||||
|
||||
it("currentLocale()", () => {
|
||||
const setLanguage = (language) => {
|
||||
Object.defineProperty(window.navigator, 'language', {
|
||||
value: language,
|
||||
writable: true
|
||||
});
|
||||
}
|
||||
setLanguage('en-EN');
|
||||
|
||||
expect(currentLocale()).equal("en");
|
||||
|
||||
setLanguage('zh-HK');
|
||||
expect(currentLocale()).equal("zh-HK");
|
||||
|
||||
// Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
|
||||
setLanguage('zh-hk');
|
||||
expect(currentLocale()).equal("en");
|
||||
|
||||
setLanguage('en-US');
|
||||
expect(currentLocale()).equal("en");
|
||||
|
||||
setLanguage('ja-ZZ');
|
||||
expect(currentLocale()).equal("ja");
|
||||
|
||||
setLanguage('zz-ZZ');
|
||||
expect(currentLocale()).equal("en");
|
||||
|
||||
setLanguage('zz-ZZ');
|
||||
expect(currentLocale()).equal("en");
|
||||
|
||||
setLanguage('en');
|
||||
localStorage.locale = "en";
|
||||
expect(currentLocale()).equal("en");
|
||||
|
||||
localStorage.locale = "zh-HK";
|
||||
expect(currentLocale()).equal("zh-HK");
|
||||
});
|
||||
|
||||
});
|
33
test/cypress/unit/util-frontend.spec.js
Normal file
33
test/cypress/unit/util-frontend.spec.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { hostNameRegexPattern } from "../../../src/util-frontend";
|
||||
|
||||
describe("Test util-frontend.js", () => {
|
||||
|
||||
describe("hostNameRegexPattern()", () => {
|
||||
it('should return a valid regex for non mqtt hostnames', () => {
|
||||
const regex = new RegExp(hostNameRegexPattern(false));
|
||||
|
||||
expect(regex.test("www.test.com")).to.be.true;
|
||||
expect(regex.test("127.0.0.1")).to.be.true;
|
||||
expect(regex.test("192.168.1.156")).to.be.true;
|
||||
|
||||
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
|
||||
expect(regex.test(`${schema}://www.test.com`)).to.be.false;
|
||||
expect(regex.test(`${schema}://127.0.0.1`)).to.be.false;
|
||||
});
|
||||
});
|
||||
it('should return a valid regex for mqtt hostnames', () => {
|
||||
const hostnameString = hostNameRegexPattern(false);
|
||||
console.log('*********', hostnameString, '***********');
|
||||
const regex = new RegExp(hostNameRegexPattern(true));
|
||||
|
||||
expect(regex.test("www.test.com")).to.be.true;
|
||||
expect(regex.test("127.0.0.1")).to.be.true;
|
||||
expect(regex.test("192.168.1.156")).to.be.true;
|
||||
|
||||
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
|
||||
expect(regex.test(`${schema}://www.test.com`)).to.be.true;
|
||||
expect(regex.test(`${schema}://127.0.0.1`)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user