Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 257718e9d6 | |||
| c1464202c7 | |||
| 5f7c451fc0 | |||
| 3d7172d9aa | |||
| 22d4597f88 | |||
| d4423e328b | |||
| b3b64806f6 | |||
| 001f6f8783 | |||
| d01f682a83 | |||
| bce523bdf1 | |||
| 623e3d5ebf | |||
| e3a7865e40 | |||
| 3c93354c7d | |||
| 323ac36b65 | |||
| c437704007 | |||
| 1953d54dc5 | |||
| b574073294 | |||
| b164602ee5 | |||
| 4bd08efc9a | |||
| 58fa09bea7 | |||
| 7ff180bea4 | |||
| 7b18806066 | |||
| d4434a64e1 | |||
| 08580794da | |||
| 70fdcc9ea6 | |||
| aba3120235 | |||
| b038d87848 | |||
| 87588a9127 | |||
| 4090fa5aef | |||
| c04561dd99 | |||
| 7f1dfcdd42 | |||
| 6f753f2901 | |||
| c39296cee6 | |||
| a1a8a8443e | |||
| f866d36db4 | |||
| 98ec911281 | |||
| 046db61326 | |||
| 3c00d0933e | |||
| 8e013f0b0b | |||
| f686134525 | |||
| 98d33a609e | |||
| d33244956e | |||
| 572c572c14 | |||
| f9743407a4 | |||
| fa3ad9b2e1 | |||
| 6bf75b2bf7 | |||
| b068c0cd51 | |||
| 92617038f0 | |||
| 40488a5a53 | |||
| 35fe0b9071 | |||
| 3bc1fd7c02 | |||
| ee4ce50193 | |||
| 67a0d1d7f4 | |||
| f006df3b02 | |||
| 5234303565 | |||
| 4ff0457966 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [tolaleng]
|
github: #[tolaleng]
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: #checkcle
|
open_collective: #checkcle
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
pocketbase.exe
|
||||||
application/src/lib/pocketbase.ts
|
application/src/lib/pocketbase.ts
|
||||||
|
server/pb_data/
|
||||||
Dockerfile
|
Dockerfile
|
||||||
application/src/lib/pocketbase-prod.ts
|
application/src/lib/pocketbase-prod.ts
|
||||||
|
|||||||
@@ -128,32 +128,30 @@ docker run -d \
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
🙏 We're incredibly thankful to our sponsors. Your contributions enable us to maintain infrastructure (hosting, domains), run tests, and continue developing valuable features.
|
Sponsorships will no longer be accepted. From now on, support will only be accepted in the form of ecosystem and community partnerships that provide infrastructure such as cloud servers, domains, or hosting credits.
|
||||||
|
|
||||||
### 🥈 Silver Uptime Ally
|
If you’re a tech company interested in supporting CheckCle, please contact the author directly at tolaleng@checkcle.io
|
||||||
|
|
||||||
<a href="https://github.com/michaelortnerit">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### 🧡 Ping Supporter
|
|
||||||
|
|
||||||
<a href="https://github.com/samang-dauth">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### 🤝 Ecosystem & Community Partner
|
### 🤝 Ecosystem & Community Partner
|
||||||
<a href="https://github.com/gitbookio" style="display: inline-block; margin-right: 10px; vertical-align: middle;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
|
<a href="https://github.com/gitbookio">
|
||||||
width="75" height="75"
|
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
|
||||||
style="border-radius: 50%;" />
|
width="75" height="75"
|
||||||
</a>
|
style="border-radius: 50%;"
|
||||||
<a href="https://m.do.co/c/0c27ef82475f" style="display: inline-block; vertical-align: middle;">
|
alt="GitBook Logo" />
|
||||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg"
|
</a>
|
||||||
height="45"
|
|
||||||
alt="DigitalOcean Logo">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
<a href="https://www.cloudflare.com">
|
||||||
|
<img src="https://cdn.checkcle.io/images/sponsor/cloudflare-checkcle_logo.png"
|
||||||
|
height="60"
|
||||||
|
alt="Cloudflare Logo" />
|
||||||
|
</a>
|
||||||
|
<a href="https://m.do.co/c/0c27ef82475f">
|
||||||
|
<img src="https://cdn.checkcle.io/images/sponsor/digitalocean_checkcle.png"
|
||||||
|
height="50"
|
||||||
|
alt="DigitalOcean Logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+285
-15
@@ -27,7 +27,7 @@
|
|||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.6",
|
||||||
"@radix-ui/react-slider": "^1.2.0",
|
"@radix-ui/react-slider": "^1.2.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
@@ -43,6 +43,8 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
"jspdf": "^3.0.1",
|
||||||
|
"jspdf-autotable": "^5.0.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"pocketbase": "^0.19.0",
|
"pocketbase": "^0.19.0",
|
||||||
@@ -91,13 +93,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.25.9",
|
"version": "7.28.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||||
"integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
|
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"regenerator-runtime": "^0.14.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -1811,12 +1810,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||||
"integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
|
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.0"
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1833,6 +1832,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slider": {
|
"node_modules/@radix-ui/react-slider": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
|
||||||
@@ -2832,6 +2887,13 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.12",
|
"version": "18.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
||||||
@@ -2853,6 +2915,13 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.11.0",
|
"version": "8.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
|
||||||
@@ -3206,6 +3275,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/atob": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||||
|
"license": "(MIT OR Apache-2.0)",
|
||||||
|
"bin": {
|
||||||
|
"atob": "bin/atob.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.20",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||||
@@ -3250,6 +3331,16 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -3318,6 +3409,18 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/btoa": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||||
|
"license": "(MIT OR Apache-2.0)",
|
||||||
|
"bin": {
|
||||||
|
"btoa": "bin/btoa.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -3358,6 +3461,26 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -3843,6 +3966,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.45.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
|
||||||
|
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3856,6 +3991,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -4064,6 +4209,16 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optional": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@@ -4424,6 +4579,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -4647,6 +4808,20 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4848,6 +5023,33 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.7",
|
||||||
|
"atob": "^2.1.2",
|
||||||
|
"btoa": "^1.2.1",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.11",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jspdf-autotable": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"jspdf": "^2 || ^3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -5208,6 +5410,13 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -5450,6 +5659,16 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@@ -5708,10 +5927,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regenerator-runtime": {
|
"node_modules/regenerator-runtime": {
|
||||||
"version": "0.14.1",
|
"version": "0.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
@@ -5750,6 +5970,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.24.0",
|
"version": "4.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
|
||||||
@@ -5883,6 +6113,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -6039,6 +6279,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
||||||
@@ -6095,6 +6345,16 @@
|
|||||||
"tailwindcss": ">=3.0.0 || insiders"
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -6314,6 +6574,16 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "0.9.9",
|
"version": "0.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -7,6 +7,7 @@ import { StatusCards } from "./StatusCards";
|
|||||||
import { ServiceFilters } from "./ServiceFilters";
|
import { ServiceFilters } from "./ServiceFilters";
|
||||||
import { ServicesTable } from "./ServicesTable";
|
import { ServicesTable } from "./ServicesTable";
|
||||||
import { AddServiceDialog } from "@/components/services/AddServiceDialog";
|
import { AddServiceDialog } from "@/components/services/AddServiceDialog";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface DashboardContentProps {
|
interface DashboardContentProps {
|
||||||
services: Service[];
|
services: Service[];
|
||||||
@@ -15,6 +16,7 @@ interface DashboardContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
|
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [filter, setFilter] = useState<string>("all");
|
const [filter, setFilter] = useState<string>("all");
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
|
||||||
@@ -31,7 +33,7 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
||||||
<p>Error loading service data.</p>
|
<p>Error loading service data.</p>
|
||||||
<Button onClick={() => window.location.reload()}>Retry</Button>
|
<Button onClick={() => window.location.reload()}>{t('retry')}</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,12 +42,12 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
|
|||||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-2xl font-bold text-foreground">Overview</h2>
|
<h2 className="text-2xl font-bold text-foreground">{t('overview')}</h2>
|
||||||
<Button
|
<Button
|
||||||
className="text-primary-foreground"
|
className="text-primary-foreground"
|
||||||
onClick={() => setIsAddDialogOpen(true)}
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" /> New Service
|
<Plus className="w-4 h-4 mr-2" /> {t('newService')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ export const Header = ({
|
|||||||
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
|
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
|
||||||
{t("Deutsch")}
|
{t("Deutsch")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
|
||||||
|
{t("korean")}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
|
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
|
||||||
{t("japanese")}
|
{t("japanese")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface ServiceFiltersProps {
|
interface ServiceFiltersProps {
|
||||||
filter: string;
|
filter: string;
|
||||||
@@ -19,10 +20,11 @@ export const ServiceFilters = ({
|
|||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
servicesCount
|
servicesCount
|
||||||
}: ServiceFiltersProps) => {
|
}: ServiceFiltersProps) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex justify-between items-center">
|
<div className="mb-6 flex justify-between items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h3 className="text-xl font-semibold mr-2 text-foreground">Currently Monitoring</h3>
|
<h3 className="text-xl font-semibold mr-2 text-foreground">{t('currentlyMonitoring')}</h3>
|
||||||
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
|
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
|
||||||
{servicesCount}
|
{servicesCount}
|
||||||
</span>
|
</span>
|
||||||
@@ -30,10 +32,10 @@ export const ServiceFilters = ({
|
|||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<Select value={filter} onValueChange={setFilter}>
|
<Select value={filter} onValueChange={setFilter}>
|
||||||
<SelectTrigger className="w-40 bg-card border-border">
|
<SelectTrigger className="w-40 bg-card border-border">
|
||||||
<SelectValue placeholder="All Types" />
|
<SelectValue placeholder={t('allTypes')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Types</SelectItem>
|
<SelectItem value="all">{t('allTypes')}</SelectItem>
|
||||||
<SelectItem value="HTTP">HTTP</SelectItem>
|
<SelectItem value="HTTP">HTTP</SelectItem>
|
||||||
<SelectItem value="PING">PING</SelectItem>
|
<SelectItem value="PING">PING</SelectItem>
|
||||||
<SelectItem value="TCP">TCP</SelectItem>
|
<SelectItem value="TCP">TCP</SelectItem>
|
||||||
@@ -43,7 +45,7 @@ export const ServiceFilters = ({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className="w-72 bg-card border-border"
|
className="w-72 bg-card border-border"
|
||||||
placeholder="Search"
|
placeholder={t('search')}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react";
|
import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react";
|
||||||
import { Service } from "@/services/serviceService";
|
import { Service } from "@/services/serviceService";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface StatusCardsProps {
|
interface StatusCardsProps {
|
||||||
services: Service[];
|
services: Service[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusCards = ({ services }: StatusCardsProps) => {
|
export const StatusCards = ({ services }: StatusCardsProps) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Count services by status
|
// Count services by status
|
||||||
const upServices = services.filter(s => s.status === "up").length;
|
const upServices = services.filter(s => s.status === "up").length;
|
||||||
const downServices = services.filter(s => s.status === "down").length;
|
const downServices = services.filter(s => s.status === "down").length;
|
||||||
@@ -42,7 +45,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="pb-2 relative z-10">
|
<CardHeader className="pb-2 relative z-10">
|
||||||
<CardTitle className="text-sm font-medium text-white">UP SERVICES</CardTitle>
|
<CardTitle className="text-sm font-medium text-white">{t("upServices")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-between relative z-10">
|
<CardContent className="flex items-center justify-between relative z-10">
|
||||||
<span className="text-5xl font-bold text-white">{upServices}</span>
|
<span className="text-5xl font-bold text-white">{upServices}</span>
|
||||||
@@ -74,7 +77,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="pb-2 relative z-10">
|
<CardHeader className="pb-2 relative z-10">
|
||||||
<CardTitle className="text-sm font-medium text-white">DOWN SERVICES</CardTitle>
|
<CardTitle className="text-sm font-medium text-white">{t("downServices")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-between relative z-10">
|
<CardContent className="flex items-center justify-between relative z-10">
|
||||||
<span className="text-5xl font-bold text-white">{downServices}</span>
|
<span className="text-5xl font-bold text-white">{downServices}</span>
|
||||||
@@ -106,7 +109,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="pb-2 relative z-10">
|
<CardHeader className="pb-2 relative z-10">
|
||||||
<CardTitle className="text-sm font-medium text-white">PAUSED SERVICES</CardTitle>
|
<CardTitle className="text-sm font-medium text-white">{t("pausedServices")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-between relative z-10">
|
<CardContent className="flex items-center justify-between relative z-10">
|
||||||
<span className="text-5xl font-bold text-white">{pausedServices}</span>
|
<span className="text-5xl font-bold text-white">{pausedServices}</span>
|
||||||
@@ -138,7 +141,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="pb-2 relative z-10">
|
<CardHeader className="pb-2 relative z-10">
|
||||||
<CardTitle className="text-sm font-medium text-white">WARNING SERVICES</CardTitle>
|
<CardTitle className="text-sm font-medium text-white">{t("warningServices")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-between relative z-10">
|
<CardContent className="flex items-center justify-between relative z-10">
|
||||||
<span className="text-5xl font-bold text-white">{warningServices}</span>
|
<span className="text-5xl font-bold text-white">{warningServices}</span>
|
||||||
|
|||||||
+6
-5
@@ -25,19 +25,20 @@ export const IncidentDetailContent = ({
|
|||||||
onClose,
|
onClose,
|
||||||
assignedUser
|
assignedUser
|
||||||
}: IncidentDetailContentProps) => {
|
}: IncidentDetailContentProps) => {
|
||||||
// Fetch assigned user details if one wasn't provided and there's an assigned_to field
|
// Fetch assigned user details if none was provided; prefer server field
|
||||||
|
const assigneeId = incident?.assigned_users || incident?.assigned_to;
|
||||||
const { data: fetchedUser } = useQuery({
|
const { data: fetchedUser } = useQuery({
|
||||||
queryKey: ['user', incident?.assigned_to],
|
queryKey: ['user', assigneeId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!incident?.assigned_to) return null;
|
if (!assigneeId) return null;
|
||||||
try {
|
try {
|
||||||
return await userService.getUser(incident.assigned_to);
|
return await userService.getUser(assigneeId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch assigned user:", error);
|
console.error("Failed to fetch assigned user:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: !!incident?.assigned_to && !assignedUser,
|
enabled: !!assigneeId && !assignedUser,
|
||||||
staleTime: 300000 // Cache for 5 minutes
|
staleTime: 300000 // Cache for 5 minutes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -17,7 +17,7 @@ export const IncidentDetailDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
incident
|
incident
|
||||||
}: IncidentDetailDialogProps) => {
|
}: IncidentDetailDialogProps) => {
|
||||||
// Fetch user data for assigned_to field
|
// Fetch user data for assigned field (prefer assigned_users, fallback to assigned_to)
|
||||||
const { data: users = [] } = useQuery({
|
const { data: users = [] } = useQuery({
|
||||||
queryKey: ['users'],
|
queryKey: ['users'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -25,12 +25,12 @@ export const IncidentDetailDialog = ({
|
|||||||
return Array.isArray(usersList) ? usersList : [];
|
return Array.isArray(usersList) ? usersList : [];
|
||||||
},
|
},
|
||||||
staleTime: 300000, // Cache for 5 minutes
|
staleTime: 300000, // Cache for 5 minutes
|
||||||
enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
|
enabled: !!(incident?.assigned_users || incident?.assigned_to) && open // Only run query if there's an assigned value and dialog is open
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find the assigned user
|
// Find the assigned user (prefer assigned_users, fallback to assigned_to)
|
||||||
const assignedUser = incident?.assigned_to
|
const assignedUser = (incident?.assigned_users || incident?.assigned_to)
|
||||||
? users.find(user => user.id === incident.assigned_to)
|
? users.find(user => user.id === (incident?.assigned_users || incident?.assigned_to))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!incident) return null;
|
if (!incident) return null;
|
||||||
|
|||||||
+5
-5
@@ -23,19 +23,19 @@ export {
|
|||||||
|
|
||||||
// Legacy component - keeping this for backward compatibility with other imports
|
// Legacy component - keeping this for backward compatibility with other imports
|
||||||
export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
|
export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
|
||||||
// Fetch assigned user details if there's an assigned_to field
|
// Fetch assigned user details if there's an assigned field (prefer assigned_users, fallback to assigned_to)
|
||||||
const { data: assignedUser } = useQuery({
|
const { data: assignedUser } = useQuery({
|
||||||
queryKey: ['user', incident?.assigned_to],
|
queryKey: ['user', incident?.assigned_users || incident?.assigned_to],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!incident?.assigned_to) return null;
|
if (!(incident?.assigned_users || incident?.assigned_to)) return null;
|
||||||
try {
|
try {
|
||||||
return await userService.getUser(incident.assigned_to);
|
return await userService.getUser(incident?.assigned_users || incident?.assigned_to);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch assigned user:", error);
|
console.error("Failed to fetch assigned user:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: !!incident?.assigned_to,
|
enabled: !!(incident?.assigned_users || incident?.assigned_to),
|
||||||
staleTime: 300000 // Cache for 5 minutes
|
staleTime: 300000 // Cache for 5 minutes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -31,8 +31,8 @@ export const AssignmentSection: React.FC<AssignmentSectionProps> = ({ incident,
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{assignedUser.full_name || assignedUser.username}</span>
|
<span>{assignedUser.full_name || assignedUser.username}</span>
|
||||||
</div>
|
</div>
|
||||||
) : incident.assigned_to ? (
|
) : (incident.assigned_users || incident.assigned_to) ? (
|
||||||
<span>{incident.assigned_to}</span>
|
<span>{incident.assigned_users || incident.assigned_to}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+2
-2
@@ -50,8 +50,8 @@ export const BasicInfoSection: React.FC<BasicInfoSectionProps> = ({ incident, as
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{assignedUser.full_name || assignedUser.username}</span>
|
<span>{assignedUser.full_name || assignedUser.username}</span>
|
||||||
</div>
|
</div>
|
||||||
) : incident.assigned_to ? (
|
) : (incident.assigned_users || incident.assigned_to) ? (
|
||||||
<span>{incident.assigned_to}</span>
|
<span>{incident.assigned_users || incident.assigned_to}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const IncidentBasicFields: React.FC = () => {
|
|||||||
const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
|
const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
|
||||||
|
|
||||||
// Function to get user initials from name
|
// Function to get user initials from name
|
||||||
const getUserInitials = (user: any): string => {
|
const getUserInitials = (user: { full_name?: string; username: string }): string => {
|
||||||
if (user.full_name) {
|
if (user.full_name) {
|
||||||
const nameParts = user.full_name.split(' ');
|
const nameParts = user.full_name.split(' ');
|
||||||
if (nameParts.length > 1) {
|
if (nameParts.length > 1) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const useIncidentEditForm = (
|
|||||||
impact: (incident.impact?.toLowerCase() || 'minor') as any,
|
impact: (incident.impact?.toLowerCase() || 'minor') as any,
|
||||||
priority: (incident.priority?.toLowerCase() || 'medium') as any,
|
priority: (incident.priority?.toLowerCase() || 'medium') as any,
|
||||||
service_id: incident.service_id || '',
|
service_id: incident.service_id || '',
|
||||||
assigned_to: incident.assigned_to || '',
|
assigned_to: incident.assigned_users || incident.assigned_to || '',
|
||||||
root_cause: incident.root_cause || '',
|
root_cause: incident.root_cause || '',
|
||||||
resolution_steps: incident.resolution_steps || '',
|
resolution_steps: incident.resolution_steps || '',
|
||||||
lessons_learned: incident.lessons_learned || '',
|
lessons_learned: incident.lessons_learned || '',
|
||||||
@@ -45,11 +45,14 @@ export const useIncidentEditForm = (
|
|||||||
impact: data.impact,
|
impact: data.impact,
|
||||||
priority: data.priority,
|
priority: data.priority,
|
||||||
service_id: data.service_id,
|
service_id: data.service_id,
|
||||||
assigned_to: data.assigned_to, // This is the user ID from the form
|
...(data.assigned_to
|
||||||
|
? { assigned_to: data.assigned_to, assigned_users: data.assigned_to }
|
||||||
|
: {}),
|
||||||
root_cause: data.root_cause,
|
root_cause: data.root_cause,
|
||||||
resolution_steps: data.resolution_steps,
|
resolution_steps: data.resolution_steps,
|
||||||
lessons_learned: data.lessons_learned,
|
lessons_learned: data.lessons_learned,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t('incidentUpdated'),
|
title: t('incidentUpdated'),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const useIncidentForm = (onSuccess: () => void, onClose: () => void) => {
|
|||||||
priority: data.priority,
|
priority: data.priority,
|
||||||
service_id: data.service_id,
|
service_id: data.service_id,
|
||||||
assigned_to: data.assigned_to,
|
assigned_to: data.assigned_to,
|
||||||
|
assigned_users: data.assigned_to, // map to server field
|
||||||
root_cause: data.root_cause,
|
root_cause: data.root_cause,
|
||||||
resolution_steps: data.resolution_steps,
|
resolution_steps: data.resolution_steps,
|
||||||
lessons_learned: data.lessons_learned,
|
lessons_learned: data.lessons_learned,
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const IncidentTableRow = memo(({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AssignedUserCell userId={localItem.assigned_to} />
|
<AssignedUserCell userId={localItem.assigned_users || localItem.assigned_to} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex justify-end items-center space-x-2">
|
<div className="flex justify-end items-center space-x-2">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { ServiceForm } from "./ServiceForm";
|
import { ServiceForm } from "./ServiceForm";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface AddServiceDialogProps {
|
interface AddServiceDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -16,6 +17,7 @@ interface AddServiceDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
|
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const handleSuccess = async () => {
|
const handleSuccess = async () => {
|
||||||
// Immediately invalidate and refetch services data
|
// Immediately invalidate and refetch services data
|
||||||
@@ -32,9 +34,9 @@ export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps)
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-xl">Create New Service</DialogTitle>
|
<DialogTitle className="text-xl">{t("createNewService")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Fill in the details to create a new service to monitor.
|
{t("createNewServiceDesc")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
|
<ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Clock, TimerOff } from "lucide-react";
|
import { Clock, TimerOff } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface LastCheckedTimeProps {
|
interface LastCheckedTimeProps {
|
||||||
lastCheckedTime: string;
|
lastCheckedTime: string;
|
||||||
@@ -10,6 +11,7 @@ interface LastCheckedTimeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheckedTimeProps) => {
|
export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheckedTimeProps) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
// Format the time without seconds to display a static time
|
// Format the time without seconds to display a static time
|
||||||
const formatTimeWithoutSeconds = (timeString: string) => {
|
const formatTimeWithoutSeconds = (timeString: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -55,7 +57,7 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
|
|||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{isPaused ? "Paused at " : ""}
|
{isPaused ? t("pausedAt") : ""}
|
||||||
{formattedTime}
|
{formattedTime}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,14 +68,14 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 text-xs">
|
<div className="flex flex-col gap-1 text-xs">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{isPaused ? "Monitoring Paused" : "Last Check Details"}
|
{isPaused ? t("monitoringPaused") : t("lastCheckDetails")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isPaused ? "No automatic checks" : `Checked at ${formattedTime}`}
|
{isPaused ? t("noAutomaticChecks") : t("checkedAt") + `${formattedTime}`}
|
||||||
</div>
|
</div>
|
||||||
{interval && !isPaused && (
|
{interval && !isPaused && (
|
||||||
<div>
|
<div>
|
||||||
Check interval: {formatInterval(interval)}
|
{t("checkInterval")}: {formatInterval(interval)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-gray-400 text-[10px]">
|
<div className="text-gray-400 text-[10px]">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
export function LoadingState() {
|
export function LoadingState() {
|
||||||
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen bg-background text-foreground">
|
<div className="flex items-center justify-center h-screen bg-background text-foreground">
|
||||||
<div className="flex flex-col items-center text-center py-8 px-4 rounded-lg shadow-lg bg-card animate-fade-in">
|
<div className="flex flex-col items-center text-center py-8 px-4 rounded-lg shadow-lg bg-card animate-fade-in">
|
||||||
@@ -7,8 +9,8 @@ export function LoadingState() {
|
|||||||
<div className="absolute w-12 h-12 rounded-full border-4 border-primary/20"></div>
|
<div className="absolute w-12 h-12 rounded-full border-4 border-primary/20"></div>
|
||||||
<div className="w-12 h-12 rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
|
<div className="w-12 h-12 rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-medium mb-1">Loading server data</h3>
|
<h3 className="text-xl font-medium mb-1">{t("loadingServerData")}</h3>
|
||||||
<p className="text-muted-foreground">Please wait while we retrieve your information...</p>
|
<p className="text-muted-foreground">{t("retrievingYourInformation")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Service } from "@/types/service.types";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface ServiceEditDialogProps {
|
interface ServiceEditDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -19,6 +20,7 @@ interface ServiceEditDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) {
|
export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -53,9 +55,9 @@ export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDi
|
|||||||
}}>
|
}}>
|
||||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-xl">Edit Service</DialogTitle>
|
<DialogTitle className="text-xl">{t("editService")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Update the details of your monitored service.
|
{t("editServiceDesc")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{open && service && (
|
{open && service && (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Service, UptimeData } from "@/types/service.types";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { formatDistanceStrict } from "date-fns";
|
import { formatDistanceStrict } from "date-fns";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceStatsCardsProps {
|
interface ServiceStatsCardsProps {
|
||||||
service: Service;
|
service: Service;
|
||||||
@@ -11,6 +12,8 @@ interface ServiceStatsCardsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) {
|
export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) {
|
||||||
|
|
||||||
|
const { t } = useLanguage();
|
||||||
// Calculate uptime percentage
|
// Calculate uptime percentage
|
||||||
const calculateUptimePercentage = () => {
|
const calculateUptimePercentage = () => {
|
||||||
if (uptimeData.length === 0) return 100;
|
if (uptimeData.length === 0) return 100;
|
||||||
@@ -109,41 +112,46 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
<Card className="border-blue-400 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/50 shadow-md">
|
<Card className="border-blue-400 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/50 shadow-md">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">Response Time</CardTitle>
|
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">{t('responseTime')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-blue-700 dark:text-white">
|
<div className="text-2xl font-bold text-blue-700 dark:text-white">
|
||||||
{service.responseTime}ms
|
{service.responseTime}ms
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Last checked at {service.lastChecked}</p>
|
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('lastCheckedAt').replace('{datetime}', service.lastChecked)}</p>
|
||||||
{avgResponseTime > 0 && avgResponseTime !== service.responseTime && (
|
{avgResponseTime > 0 && avgResponseTime !== service.responseTime && (
|
||||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Avg: {avgResponseTime}ms (last {upChecks.length} up checks)</p>
|
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('avg')}: {avgResponseTime}ms ({t('lastUpChecksCount').replace('{count}', String(upChecks.length))})</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="border-green-400 dark:border-green-700 bg-green-50 dark:bg-green-900/50 shadow-md">
|
<Card className="border-green-400 dark:border-green-700 bg-green-50 dark:bg-green-900/50 shadow-md">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">Uptime</CardTitle>
|
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">{t('uptime')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-700 dark:text-white">
|
<div className="text-2xl font-bold text-green-700 dark:text-white">
|
||||||
{uptimePercentage}%
|
{uptimePercentage}%
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-green-600 dark:text-green-200 mt-1">Based on last {uptimeData.length} checks</p>
|
<p className="text-xs text-green-600 dark:text-green-200 mt-1">{t('basedOnlastChecksCount').replace('{count}', String(uptimeData.length))}</p>
|
||||||
<div className="flex flex-col space-y-1 mt-2">
|
<div className="flex flex-col space-y-1 mt-2">
|
||||||
<div className="flex items-center text-xs text-green-600 dark:text-green-200">
|
<div className="flex items-center text-xs text-green-600 dark:text-green-200">
|
||||||
<ArrowUp className="h-3 w-3 mr-1 text-green-600 dark:text-green-400" />
|
<ArrowUp className="h-3 w-3 mr-1 text-green-600 dark:text-green-400" />
|
||||||
<span>Total uptime: {uptimeStats.totalUptimeFormatted}</span>
|
<span>{t("totalUptime")}: {uptimeStats.totalUptimeFormatted}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-red-600 dark:text-red-200">
|
<div className="flex items-center text-xs text-red-600 dark:text-red-200">
|
||||||
<ArrowDown className="h-3 w-3 mr-1 text-red-600 dark:text-red-400" />
|
<ArrowDown className="h-3 w-3 mr-1 text-red-600 dark:text-red-400" />
|
||||||
<span>Total downtime: {uptimeStats.totalDowntimeFormatted}</span>
|
<span>{t("totalDowntime")}: {uptimeStats.totalDowntimeFormatted}</span>
|
||||||
</div>
|
</div>
|
||||||
{service.status !== "paused" && (
|
{service.status !== "paused" && (
|
||||||
<div className="flex items-center text-xs font-medium mt-1">
|
<div className="flex items-center text-xs font-medium mt-1">
|
||||||
<span className={service.status === "up" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
|
<span className={service.status === "up" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
|
||||||
{service.status === "up" ? "Up" : "Down"} for {uptimeStats.currentStatusDuration}
|
{
|
||||||
|
service.status === "up" ?
|
||||||
|
t('upStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
|
||||||
|
:
|
||||||
|
t('downStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -153,16 +161,16 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
|
|||||||
|
|
||||||
<Card className="border-purple-400 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/50 shadow-md">
|
<Card className="border-purple-400 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/50 shadow-md">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">Monitoring Settings</CardTitle>
|
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">{t('monitoringSettings')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200">
|
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200">
|
||||||
<Clock className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
<Clock className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
||||||
<span>Checked every {service.interval} seconds</span>
|
<span>{t('monitoringSettingsInterval').replace('{interval}', String(service.interval))}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200 mt-1">
|
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200 mt-1">
|
||||||
<Server className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
<Server className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
||||||
<span>{service.type} monitoring</span>
|
<span>{service.type} {t('monitoringSettingsType')}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ export function ServicesPagination({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
{totalItems > 0
|
{totalItems > 0
|
||||||
? `${startItem}-${endItem} of ${totalItems} ${t("services") || "services"}`
|
? t("servicesPagination", {"startItem": startItem, "endItem": endItem, "totalItems": totalItems})
|
||||||
: `0 ${t("services") || "services"}`
|
: t("servicesPaginationNoService")
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
|||||||
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
|
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
|
||||||
import { uptimeService } from '@/services/uptimeService';
|
import { uptimeService } from '@/services/uptimeService';
|
||||||
import { UptimeData } from '@/types/service.types';
|
import { UptimeData } from '@/types/service.types';
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface UptimeBarProps {
|
interface UptimeBarProps {
|
||||||
uptime: number;
|
uptime: number;
|
||||||
@@ -15,6 +16,7 @@ interface UptimeBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
|
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
// Calculate date range for last 20 checks with much more aggressive caching
|
// Calculate date range for last 20 checks with much more aggressive caching
|
||||||
const endDate = useMemo(() => new Date(), []);
|
const endDate = useMemo(() => new Date(), []);
|
||||||
const startDate = useMemo(() => {
|
const startDate = useMemo(() => {
|
||||||
@@ -84,7 +86,7 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Last 20 checks
|
{t('last20Checks')}
|
||||||
</span>
|
</span>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,22 +3,24 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/comp
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { ServiceFormData } from "./types";
|
import { ServiceFormData } from "./types";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceBasicFieldsProps {
|
interface ServiceBasicFieldsProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceBasicFields({ form }: ServiceBasicFieldsProps) {
|
export function ServiceBasicFields({ form }: ServiceBasicFieldsProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Service Name</FormLabel>
|
<FormLabel>{t('serviceName')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a descriptive name for your service"
|
placeholder={t('serviceNameDesc')}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { UseFormReturn } from "react-hook-form";
|
|||||||
import { ServiceFormData } from "./types";
|
import { ServiceFormData } from "./types";
|
||||||
import { ServiceUrlField } from "./ServiceUrlField";
|
import { ServiceUrlField } from "./ServiceUrlField";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceConfigFieldsProps {
|
interface ServiceConfigFieldsProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [isCustomInterval, setIsCustomInterval] = useState(false);
|
const [isCustomInterval, setIsCustomInterval] = useState(false);
|
||||||
const intervalValue = form.watch("interval");
|
const intervalValue = form.watch("interval");
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
|||||||
name="interval"
|
name="interval"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Check Interval</FormLabel>
|
<FormLabel>{t("checkInterval")}</FormLabel>
|
||||||
{!isCustomInterval ? (
|
{!isCustomInterval ? (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}>
|
<Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}>
|
||||||
@@ -43,13 +45,13 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="30">30 seconds</SelectItem>
|
<SelectItem value="30">30 {t("seconds")}</SelectItem>
|
||||||
<SelectItem value="60">1 minute</SelectItem>
|
<SelectItem value="60">1 {t("minute")}</SelectItem>
|
||||||
<SelectItem value="300">5 minutes</SelectItem>
|
<SelectItem value="300">5 {t("minutes")}</SelectItem>
|
||||||
<SelectItem value="900">15 minutes</SelectItem>
|
<SelectItem value="900">15 {t("minutes")}</SelectItem>
|
||||||
<SelectItem value="1800">30 minutes</SelectItem>
|
<SelectItem value="1800">30 {t("minutes")}</SelectItem>
|
||||||
<SelectItem value="3600">1 hour</SelectItem>
|
<SelectItem value="3600">1 {t("hour")}</SelectItem>
|
||||||
<SelectItem value="custom">Custom</SelectItem>
|
<SelectItem value="custom">{t("custom")}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -58,7 +60,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Enter interval in seconds"
|
placeholder={t("checkIntervalPlaceholder")}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
min="10"
|
min="10"
|
||||||
@@ -72,14 +74,14 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
|||||||
}}
|
}}
|
||||||
className="text-sm text-blue-600 hover:text-blue-800"
|
className="text-sm text-blue-600 hover:text-blue-800"
|
||||||
>
|
>
|
||||||
Back to presets
|
{t("backToPresets")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FormDescription className="text-xs">
|
<FormDescription className="text-xs">
|
||||||
{isCustomInterval
|
{isCustomInterval
|
||||||
? "Enter custom interval in seconds (minimum 10 seconds)"
|
? t("checkIntervalDescCustom")
|
||||||
: "How often to check the service status"
|
: t("checkIntervalDesc")
|
||||||
}
|
}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -92,22 +94,22 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
|||||||
name="retries"
|
name="retries"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Retry Attempts</FormLabel>
|
<FormLabel>{t("retryAttempts")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} value={field.value ?? "3"}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1">1 attempt</SelectItem>
|
<SelectItem value="1">1 {t("attempt")}</SelectItem>
|
||||||
<SelectItem value="2">2 attempts</SelectItem>
|
<SelectItem value="2">2 {t("attempts")}</SelectItem>
|
||||||
<SelectItem value="3">3 attempts</SelectItem>
|
<SelectItem value="3">3 {t("attempts")}</SelectItem>
|
||||||
<SelectItem value="5">5 attempts</SelectItem>
|
<SelectItem value="5">5 {t("attempts")}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription className="text-xs">
|
<FormDescription className="text-xs">
|
||||||
Number of retry attempts before marking as down
|
{t("retryAttemptsDesc")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { serviceSchema, ServiceFormData } from "./types";
|
import { useServiceSchema, ServiceFormData } from "./types";
|
||||||
import { ServiceBasicFields } from "./ServiceBasicFields";
|
import { ServiceBasicFields } from "./ServiceBasicFields";
|
||||||
import { ServiceTypeField } from "./ServiceTypeField";
|
import { ServiceTypeField } from "./ServiceTypeField";
|
||||||
import { ServiceConfigFields } from "./ServiceConfigFields";
|
import { ServiceConfigFields } from "./ServiceConfigFields";
|
||||||
@@ -14,6 +14,7 @@ import { Service } from "@/types/service.types";
|
|||||||
import { ServiceRegionalFields } from "./ServiceRegionalFields";
|
import { ServiceRegionalFields } from "./ServiceRegionalFields";
|
||||||
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
|
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceFormProps {
|
interface ServiceFormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@@ -30,10 +31,13 @@ export function ServiceForm({
|
|||||||
isEdit = false,
|
isEdit = false,
|
||||||
onSubmitStart
|
onSubmitStart
|
||||||
}: ServiceFormProps) {
|
}: ServiceFormProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const serviceSchema = useServiceSchema();
|
||||||
|
|
||||||
// Initialize form with default values
|
// Initialize form with default values
|
||||||
const form = useForm<ServiceFormData>({
|
const form = useForm<ServiceFormData>({
|
||||||
resolver: zodResolver(serviceSchema),
|
resolver: zodResolver(serviceSchema),
|
||||||
@@ -124,23 +128,23 @@ export function ServiceForm({
|
|||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('basicInformation')}</h3>
|
||||||
<ServiceBasicFields form={form} />
|
<ServiceBasicFields form={form} />
|
||||||
<ServiceTypeField form={form} />
|
<ServiceTypeField form={form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('configuration')}</h3>
|
||||||
<ServiceConfigFields form={form} />
|
<ServiceConfigFields form={form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('regionalMonitoring')}</h3>
|
||||||
<ServiceRegionalFields form={form} />
|
<ServiceRegionalFields form={form} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('notifications')}</h3>
|
||||||
<ServiceNotificationFields form={form} />
|
<ServiceNotificationFields form={form} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +152,7 @@ export function ServiceForm({
|
|||||||
<ServiceFormActions
|
<ServiceFormActions
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
submitLabel={isEdit ? "Update Service" : "Create Service"}
|
submitLabel={isEdit ? t("updateService") : t("createService")}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceFormActionsProps {
|
interface ServiceFormActionsProps {
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
@@ -14,6 +15,8 @@ export function ServiceFormActions({
|
|||||||
onCancel,
|
onCancel,
|
||||||
submitLabel = "Create Service"
|
submitLabel = "Create Service"
|
||||||
}: ServiceFormActionsProps) {
|
}: ServiceFormActionsProps) {
|
||||||
|
|
||||||
|
const {t} = useLanguage();
|
||||||
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isSubmitting) {
|
if (!isSubmitting) {
|
||||||
@@ -29,7 +32,7 @@ export function ServiceFormActions({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Cancel
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -38,7 +41,7 @@ export function ServiceFormActions({
|
|||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Processing...
|
{t("processing")}...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
submitLabel
|
submitLabel
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||||
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
|
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceNotificationFieldsProps {
|
interface ServiceNotificationFieldsProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceNotificationFields({ form }: ServiceNotificationFieldsProps) {
|
export function ServiceNotificationFields({ form }: ServiceNotificationFieldsProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||||
|
|
||||||
// Get the current form values for debugging
|
// Get the current form values for debugging
|
||||||
@@ -88,10 +90,10 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base">
|
<FormLabel className="text-base">
|
||||||
Enable Notifications
|
{t("enableNotifications")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enable or disable notifications for this service
|
{t("enableNotificationsDesc")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -115,11 +117,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
name="notificationChannels"
|
name="notificationChannels"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Notification Channels</FormLabel>
|
<FormLabel>{t("notificationChannels")}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{notificationStatus === "enabled"
|
{notificationStatus === "enabled"
|
||||||
? "Select notification channels for this service"
|
? t("notificationChannelsEnabledDesc")
|
||||||
: "Enable notifications first to select channels"}
|
: t("notificationChannelsDesc")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
||||||
{/* Display selected channels as badges */}
|
{/* Display selected channels as badges */}
|
||||||
@@ -144,7 +146,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
value="" // Always reset to empty after selection
|
value="" // Always reset to empty after selection
|
||||||
>
|
>
|
||||||
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||||
<SelectValue placeholder="Add a notification channel" />
|
<SelectValue placeholder={t("notificationChannelsPlaceholder")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{alertConfigs
|
{alertConfigs
|
||||||
@@ -169,7 +171,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Alert Template</FormLabel>
|
<FormLabel>{t("alertTemplate")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -179,7 +181,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
|
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||||
<SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
|
<SelectValue placeholder={isLoadingTemplates ? t("alertTemplateLoading") : t("alertTemplatePlaceholder")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{serviceTemplates?.map((template) => (
|
{serviceTemplates?.map((template) => (
|
||||||
@@ -192,8 +194,8 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{notificationStatus === "enabled"
|
{notificationStatus === "enabled"
|
||||||
? "Choose a template for alert messages"
|
? t("alertTemplateEnabledDesc")
|
||||||
: "Enable notifications first to select template"}
|
: t("alertTemplateDesc")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { regionalService } from "@/services/regionalService";
|
|||||||
import { MapPin, Loader2, X, Plus } from "lucide-react";
|
import { MapPin, Loader2, X, Plus } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceRegionalFieldsProps {
|
interface ServiceRegionalFieldsProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
|
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
|
||||||
const currentRegionalAgents = form.watch("regionalAgents") || [];
|
const currentRegionalAgents = form.watch("regionalAgents") || [];
|
||||||
|
|
||||||
@@ -79,10 +81,10 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base font-medium flex items-center gap-2">
|
<FormLabel className="text-base font-medium flex items-center gap-2">
|
||||||
<MapPin className="h-4 w-4" />
|
<MapPin className="h-4 w-4" />
|
||||||
Regional Monitoring
|
{t("regionalMonitoring")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Assign this service to regional monitoring agents for distributed monitoring
|
{t("regionalMonitoringDesc")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -107,7 +109,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
name="regionalAgents"
|
name="regionalAgents"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Regional Agents</FormLabel>
|
<FormLabel>{t("regionalAgents")}</FormLabel>
|
||||||
|
|
||||||
{/* Display selected agents */}
|
{/* Display selected agents */}
|
||||||
{currentRegionalAgents.length > 0 && (
|
{currentRegionalAgents.length > 0 && (
|
||||||
@@ -140,12 +142,12 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={
|
<SelectValue placeholder={
|
||||||
isLoading
|
isLoading
|
||||||
? "Loading agents..."
|
? t("regionalAgentsLoading")
|
||||||
: availableAgents.length > 0
|
: availableAgents.length > 0
|
||||||
? "Select additional regional agents..."
|
? t("regionalAgentsAvailablePlaceholder")
|
||||||
: currentRegionalAgents.length > 0
|
: currentRegionalAgents.length > 0
|
||||||
? "All available agents selected"
|
? t("regionalAgentsAllSelected")
|
||||||
: "No regional agents available"
|
: t("regionalAgentsNoAvailable")
|
||||||
} />
|
} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -154,14 +156,14 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
<SelectItem value="loading" disabled>
|
<SelectItem value="loading" disabled>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
Loading agents...
|
t("regionalAgentsLoading")
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : availableAgents.length === 0 ? (
|
) : availableAgents.length === 0 ? (
|
||||||
<SelectItem value="no-agents" disabled>
|
<SelectItem value="no-agents" disabled>
|
||||||
{currentRegionalAgents.length > 0
|
{currentRegionalAgents.length > 0
|
||||||
? "All available agents selected"
|
? t("regionalAgentsAllSelected")
|
||||||
: "No online regional agents available"
|
: t("regionalAgentsNoOnlineAvailable")
|
||||||
}
|
}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : (
|
) : (
|
||||||
@@ -190,13 +192,13 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
|
|
||||||
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
|
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
|
||||||
<p className="text-sm text-amber-600">
|
<p className="text-sm text-amber-600">
|
||||||
No online regional agents found. Services will use default monitoring.
|
{t("regionalAgentsNotFoundMessage")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
|
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
|
||||||
<p className="text-sm text-orange-600">
|
<p className="text-sm text-orange-600">
|
||||||
No regional agents selected. Service will use default monitoring.
|
{t("regionalAgentsNotSelectedMessage")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Globe, Wifi, Server, Globe2 } from "lucide-react";
|
import { Globe, Wifi, Server, Globe2 } from "lucide-react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { ServiceFormData } from "./types";
|
import { ServiceFormData } from "./types";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceTypeFieldProps {
|
interface ServiceTypeFieldProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const getServiceIcon = (type: string) => {
|
const getServiceIcon = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "http":
|
case "http":
|
||||||
@@ -31,7 +33,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
name="type"
|
name="type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Service Type</FormLabel>
|
<FormLabel>{t("serviceType")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
@@ -56,7 +58,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
<span>HTTP/S</span>
|
<span>HTTP/S</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Monitor websites and REST APIs with HTTP/HTTPS Protocol
|
{t("serviceTypeHTTPDesc")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -67,7 +69,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
<span>PING</span>
|
<span>PING</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Monitor host availability with PING Protocol
|
{t("serviceTypePINGDesc")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -78,7 +80,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
<span>TCP</span>
|
<span>TCP</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Monitor TCP port connectivity with TCP Protocol
|
{t("serviceTypeTCPDesc")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -89,7 +91,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
<span>DNS</span>
|
<span>DNS</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Monitor DNS resolution
|
{t("serviceTypeDNSDesc")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescripti
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { ServiceFormData } from "./types";
|
import { ServiceFormData } from "./types";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface ServiceUrlFieldProps {
|
interface ServiceUrlFieldProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
const serviceType = form.watch("type");
|
const serviceType = form.watch("type");
|
||||||
|
|
||||||
const getPlaceholder = () => {
|
const getPlaceholder = () => {
|
||||||
@@ -22,35 +24,35 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
|||||||
case "dns":
|
case "dns":
|
||||||
return "example.com";
|
return "example.com";
|
||||||
default:
|
default:
|
||||||
return "Enter URL or hostname";
|
return t("targetDefaultPlaceholder");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
switch (serviceType) {
|
switch (serviceType) {
|
||||||
case "http":
|
case "http":
|
||||||
return "Enter the full URL including protocol (http:// or https://)";
|
return t("targetHTTPDesc");
|
||||||
case "ping":
|
case "ping":
|
||||||
return "Enter hostname or IP address to ping";
|
return t("targetPINGDesc");
|
||||||
case "tcp":
|
case "tcp":
|
||||||
return "Enter hostname or IP address for TCP connection test";
|
return t("targetTCPDesc");
|
||||||
case "dns":
|
case "dns":
|
||||||
return "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)";
|
return t("targetDNSDesc");
|
||||||
default:
|
default:
|
||||||
return "Enter the target URL or hostname for monitoring";
|
return t("targetDefaultDesc");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFieldLabel = () => {
|
const getFieldLabel = () => {
|
||||||
switch (serviceType) {
|
switch (serviceType) {
|
||||||
case "dns":
|
case "dns":
|
||||||
return "Domain Name";
|
return t("targetDNS");
|
||||||
case "ping":
|
case "ping":
|
||||||
return "Hostname/IP";
|
return "Hostname/IP";
|
||||||
case "tcp":
|
case "tcp":
|
||||||
return "Hostname/IP";
|
return "Hostname/IP";
|
||||||
default:
|
default:
|
||||||
return "Target URL/Host";
|
return t("targetDefault");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,7 +101,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription className="text-xs">
|
<FormDescription className="text-xs">
|
||||||
Enter the port number for TCP connection test
|
{t("targetTCPPortDesc")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import type { ZodSchema } from "zod";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
export const serviceSchema = z.object({
|
|
||||||
name: z.string().min(1, "Service name is required"),
|
|
||||||
type: z.enum(["http", "ping", "tcp", "dns"]),
|
|
||||||
url: z.string().min(1, "URL/Domain/Host is required").refine((value) => {
|
|
||||||
// Basic validation - more specific validation can be added per type
|
|
||||||
return value.trim().length > 0;
|
|
||||||
}, "Please enter a valid URL, hostname, or domain"),
|
|
||||||
port: z.string().optional(),
|
|
||||||
interval: z.string(),
|
|
||||||
retries: z.string(),
|
|
||||||
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
|
||||||
notificationChannels: z.array(z.string()).optional(),
|
|
||||||
alertTemplate: z.string().optional(),
|
|
||||||
// Regional monitoring fields - now supports multiple agents
|
|
||||||
regionalMonitoringEnabled: z.boolean().optional(),
|
|
||||||
regionalAgents: z.array(z.string()).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ServiceFormData = z.infer<typeof serviceSchema>;
|
export type ServiceFormData = {
|
||||||
|
name: string;
|
||||||
|
type: "http" | "ping" | "tcp" | "dns";
|
||||||
|
url: string;
|
||||||
|
port?: string;
|
||||||
|
interval: string;
|
||||||
|
retries: string;
|
||||||
|
notificationStatus?: "enabled" | "disabled";
|
||||||
|
notificationChannels?: string[];
|
||||||
|
alertTemplate?: string;
|
||||||
|
regionalMonitoringEnabled?: boolean;
|
||||||
|
regionalAgents?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook to use the schema with translations
|
||||||
|
export const useServiceSchema = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
return z.object({
|
||||||
|
name: z.string().min(1, t("serviceNameRequired")),
|
||||||
|
type: z.enum(["http", "ping", "tcp", "dns"]),
|
||||||
|
url: z.string()
|
||||||
|
.min(1, t("urlDomainHostRequired"))
|
||||||
|
.refine(
|
||||||
|
(value) => value.trim().length > 0,
|
||||||
|
t("enterValidUrlHostnameDomain")
|
||||||
|
),
|
||||||
|
port: z.string().optional(),
|
||||||
|
interval: z.string(),
|
||||||
|
retries: z.string(),
|
||||||
|
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
||||||
|
notificationChannels: z.array(z.string()).optional(),
|
||||||
|
alertTemplate: z.string().optional(),
|
||||||
|
// Regional monitoring fields - now supports multiple agents
|
||||||
|
regionalMonitoringEnabled: z.boolean().optional(),
|
||||||
|
regionalAgents: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -25,8 +25,8 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
|
|||||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead>
|
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead>
|
||||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead>
|
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead>
|
||||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead>
|
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead>
|
||||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Error Message</TableHead>
|
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("errorMessage")}</TableHead>
|
||||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Details</TableHead>
|
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("details")}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import { IncidentTable } from "./IncidentTable";
|
|||||||
import { StatusFilter, PageSize } from "./types";
|
import { StatusFilter, PageSize } from "./types";
|
||||||
import { getStatusChangeEvents } from "./utils";
|
import { getStatusChangeEvents } from "./utils";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] }) {
|
export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] }) {
|
||||||
// Get current theme
|
// Get current theme
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||||
@@ -65,7 +67,7 @@ export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] })
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<CardTitle className="text-card-foreground">
|
<CardTitle className="text-card-foreground">
|
||||||
<span>Incident History</span>
|
<span>{t("incidentHistory")}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<StatusFilterTabs statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} />
|
<StatusFilterTabs statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SelectValue
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { PageSize } from "./types";
|
import { PageSize } from "./types";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface TablePaginationProps {
|
interface TablePaginationProps {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
@@ -35,6 +36,7 @@ export function TablePagination({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
}: TablePaginationProps) {
|
}: TablePaginationProps) {
|
||||||
|
const {t} = useLanguage()
|
||||||
// Generate page numbers
|
// Generate page numbers
|
||||||
const getPageNumbers = () => {
|
const getPageNumbers = () => {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
@@ -58,7 +60,7 @@ export function TablePagination({
|
|||||||
return (
|
return (
|
||||||
<div className="mt-4 flex items-center justify-between">
|
<div className="mt-4 flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-sm text-muted-foreground">Rows per page:</span>
|
<span className="text-sm text-muted-foreground">{t("rowsPerPage")}:</span>
|
||||||
<Select
|
<Select
|
||||||
value={pageSize}
|
value={pageSize}
|
||||||
onValueChange={(value) => onPageSizeChange(value as PageSize)}
|
onValueChange={(value) => onPageSizeChange(value as PageSize)}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
import { serviceService } from "@/services/serviceService";
|
import { serviceService } from "@/services/serviceService";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface ServiceRowActionsProps {
|
interface ServiceRowActionsProps {
|
||||||
service: Service;
|
service: Service;
|
||||||
@@ -31,6 +32,7 @@ export const ServiceRowActions = ({
|
|||||||
onMuteAlerts
|
onMuteAlerts
|
||||||
}: ServiceRowActionsProps) => {
|
}: ServiceRowActionsProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Handle pause/resume directly from dropdown
|
// Handle pause/resume directly from dropdown
|
||||||
const handlePauseResume = async (e: React.MouseEvent) => {
|
const handlePauseResume = async (e: React.MouseEvent) => {
|
||||||
@@ -122,7 +124,7 @@ export const ServiceRowActions = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
<span>View Detail</span>
|
<span>{t("viewDetail")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
||||||
@@ -131,12 +133,12 @@ export const ServiceRowActions = ({
|
|||||||
{service.status === "paused" ? (
|
{service.status === "paused" ? (
|
||||||
<>
|
<>
|
||||||
<Play className="h-4 w-4" />
|
<Play className="h-4 w-4" />
|
||||||
<span>Resume Monitoring</span>
|
<span>{t("resumeMonitoring")}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Pause className="h-4 w-4" />
|
<Pause className="h-4 w-4" />
|
||||||
<span>Pause Monitoring</span>
|
<span>{t("pauseMonitoring")}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -148,7 +150,7 @@ export const ServiceRowActions = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
<span>Edit</span>
|
<span>{t("edit")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
||||||
@@ -157,12 +159,12 @@ export const ServiceRowActions = ({
|
|||||||
{alertsMuted ? (
|
{alertsMuted ? (
|
||||||
<>
|
<>
|
||||||
<Bell className="h-4 w-4" />
|
<Bell className="h-4 w-4" />
|
||||||
<span>Unmute Alerts</span>
|
<span>{t("unmuteAlerts")}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<BellOff className="h-4 w-4" />
|
<BellOff className="h-4 w-4" />
|
||||||
<span>Mute Alerts</span>
|
<span>{t("muteAlerts")}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -175,7 +177,7 @@ export const ServiceRowActions = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
<span>Delete</span>
|
<span>{t("delete")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
interface UptimeSummaryProps {
|
interface UptimeSummaryProps {
|
||||||
uptime: number;
|
uptime: number;
|
||||||
@@ -7,13 +8,14 @@ interface UptimeSummaryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
|
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between text-xs mt-1">
|
<div className="flex items-center justify-between text-xs mt-1">
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{Math.round(uptime)}% uptime
|
{Math.round(uptime)}% uptime
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Last 20 checks
|
{t('last20Checks')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,50 +1,293 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { format } from 'date-fns';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
|
||||||
Card, CardContent, CardDescription, CardHeader, CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon, FolderOpen, Database, CheckCircle } from "lucide-react";
|
||||||
Github, FileText, Twitter, MessageCircle, Code2, ServerIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export const AboutSystem: React.FC = () => {
|
export const AboutSystem: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { systemName } = useSystemSettings();
|
const { systemName } = useSystemSettings();
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [mergeFields, setMergeFields] = useState(true);
|
||||||
|
const [importResult, setImportResult] = useState<{
|
||||||
|
success: boolean;
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
skipped: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const [version, setVersion] = useState<string>('...');
|
// Helper function to check if a collection is a system collection
|
||||||
const [releaseDate, setReleaseDate] = useState<string>('...');
|
const isSystemCollection = (name: string): boolean => {
|
||||||
|
return name.startsWith('_') || ['users'].includes(name);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Helper function to validate collection schema
|
||||||
const fetchLatestRelease = async () => {
|
const validateCollectionSchema = (collection: any): string | null => {
|
||||||
try {
|
if (!collection.name) return "Collection name is required";
|
||||||
const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest');
|
if (typeof collection.name !== 'string') return "Collection name must be a string";
|
||||||
const data = await res.json();
|
if (!/^[a-z][a-z0-9_]*$/.test(collection.name)) {
|
||||||
setVersion(data.tag_name || 'v1.x.x');
|
return "Collection name must start with a letter and contain only lowercase letters, numbers, and underscores";
|
||||||
setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown'));
|
}
|
||||||
} catch (err) {
|
if (collection.name.length < 3 || collection.name.length > 32) {
|
||||||
setVersion('v1.x.x');
|
return "Collection name must be between 3 and 32 characters";
|
||||||
setReleaseDate(t('unknown'));
|
}
|
||||||
|
|
||||||
|
// Validate schema array if present
|
||||||
|
if (collection.schema && !Array.isArray(collection.schema)) {
|
||||||
|
return "Collection schema must be an array";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each field in the schema
|
||||||
|
if (collection.schema) {
|
||||||
|
for (const field of collection.schema) {
|
||||||
|
if (!field.name || typeof field.name !== 'string') {
|
||||||
|
return `Invalid field name in collection '${collection.name}'`;
|
||||||
|
}
|
||||||
|
if (!field.type || typeof field.type !== 'string') {
|
||||||
|
return `Invalid field type for '${field.name}' in collection '${collection.name}'`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
fetchLatestRelease();
|
|
||||||
}, [t]);
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSchemaImport = async (schemaData: string) => {
|
||||||
|
if (!schemaData.trim()) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "No schema data provided",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsImporting(true);
|
||||||
|
setImportResult(null);
|
||||||
|
|
||||||
|
// Parse the JSON to validate it
|
||||||
|
const collections = JSON.parse(schemaData);
|
||||||
|
|
||||||
|
if (!Array.isArray(collections)) {
|
||||||
|
throw new Error("Schema must be an array of collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is authenticated as admin
|
||||||
|
if (!pb.authStore.isValid) {
|
||||||
|
throw new Error("Authentication required. Please log in as an admin.");
|
||||||
|
}
|
||||||
|
// Create/update collections one by one using PocketBase client
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
let updatedCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const collection of collections) {
|
||||||
|
try {
|
||||||
|
// Skip system collections
|
||||||
|
if (isSystemCollection(collection.name)) {
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate collection schema
|
||||||
|
const validationError = validateCollectionSchema(collection);
|
||||||
|
if (validationError) {
|
||||||
|
throw new Error(validationError);
|
||||||
|
}
|
||||||
|
// Check if collection already exists
|
||||||
|
const existingCollections = await pb.collections.getFullList();
|
||||||
|
const existingCollection = existingCollections.find(c => c.name === collection.name);
|
||||||
|
|
||||||
|
if (existingCollection) {
|
||||||
|
if (mergeFields) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get existing schema
|
||||||
|
const existingSchema = existingCollection.schema || [];
|
||||||
|
const newSchema = collection.schema || [];
|
||||||
|
// Merge schemas - add new fields, keep existing ones
|
||||||
|
const mergedSchema = [...existingSchema];
|
||||||
|
let fieldsAdded = 0;
|
||||||
|
|
||||||
|
for (const newField of newSchema) {
|
||||||
|
const existingFieldIndex = mergedSchema.findIndex(f => f.name === newField.name);
|
||||||
|
if (existingFieldIndex >= 0) {
|
||||||
|
} else {
|
||||||
|
// Add new field
|
||||||
|
mergedSchema.push(newField);
|
||||||
|
fieldsAdded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the collection with merged schema
|
||||||
|
const updateData = {
|
||||||
|
...collection,
|
||||||
|
schema: mergedSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedCollection = await pb.collections.update(existingCollection.id, updateData);
|
||||||
|
updatedCount++;
|
||||||
|
} catch (mergeError) {
|
||||||
|
throw new Error(`Failed to merge fields: ${mergeError.message || mergeError}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new collection
|
||||||
|
try {
|
||||||
|
const newCollection = await pb.collections.create(collection);
|
||||||
|
successCount++;
|
||||||
|
} catch (createError) {
|
||||||
|
|
||||||
|
// Extract more detailed error information
|
||||||
|
let errorMessage = createError.message || createError;
|
||||||
|
if (createError.data) {
|
||||||
|
// Try to extract field-specific errors
|
||||||
|
if (createError.data.data) {
|
||||||
|
const fieldErrors = Object.entries(createError.data.data).map(([field, msgs]) =>
|
||||||
|
`${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}`
|
||||||
|
).join('; ');
|
||||||
|
if (fieldErrors) {
|
||||||
|
errorMessage += ` - Field errors: ${fieldErrors}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to create collection: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
const errorMsg = `Failed to process collection '${collection.name}': ${error.message || error}`;
|
||||||
|
errors.push(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set import result for display
|
||||||
|
setImportResult({
|
||||||
|
success: successCount + updatedCount > 0,
|
||||||
|
created: successCount,
|
||||||
|
updated: updatedCount,
|
||||||
|
skipped: skippedCount
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show results
|
||||||
|
const totalProcessed = successCount + updatedCount;
|
||||||
|
if (totalProcessed > 0 || skippedCount > 0) {
|
||||||
|
let description = '';
|
||||||
|
if (totalProcessed > 0) {
|
||||||
|
description = `Successfully processed ${totalProcessed} collection(s)`;
|
||||||
|
if (successCount > 0) description += ` (${successCount} created)`;
|
||||||
|
if (updatedCount > 0) description += ` (${updatedCount} updated)`;
|
||||||
|
}
|
||||||
|
if (skippedCount > 0) {
|
||||||
|
if (description) description += `, `;
|
||||||
|
description += `${skippedCount} skipped (system collections or existing)`;
|
||||||
|
}
|
||||||
|
if (errorCount > 0) description += `. ${errorCount} failed.`;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Import Complete",
|
||||||
|
description,
|
||||||
|
variant: totalProcessed > errorCount ? "default" : "destructive",
|
||||||
|
});
|
||||||
|
} else if (errorCount > 0) {
|
||||||
|
toast({
|
||||||
|
title: "Import Failed",
|
||||||
|
description: "No collections were processed successfully.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "No Changes",
|
||||||
|
description: "All collections already exist and no changes were made.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
// Show detailed errors for debugging
|
||||||
|
if (errors.length <= 3) {
|
||||||
|
toast({
|
||||||
|
title: "Detailed Errors",
|
||||||
|
description: errors.join('; '),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Import Failed",
|
||||||
|
description: error instanceof Error ? error.message : "Failed to import schema",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setImportResult({
|
||||||
|
success: false,
|
||||||
|
created: 0,
|
||||||
|
updated: 0,
|
||||||
|
skipped: 0
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadLocalSchema = async () => {
|
||||||
|
try {
|
||||||
|
setIsImporting(true);
|
||||||
|
setImportResult(null);
|
||||||
|
|
||||||
|
const response = await fetch('/upload/data/pb_schema_latest.json');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load local schema: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaText = await response.text();
|
||||||
|
|
||||||
|
// Validate that it's valid JSON
|
||||||
|
JSON.parse(schemaText);
|
||||||
|
|
||||||
|
// Directly import the schema
|
||||||
|
await handleSchemaImport(schemaText);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Import Failed",
|
||||||
|
description: error instanceof Error ? error.message : "Failed to load and import local schema file",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setImportResult({
|
||||||
|
success: false,
|
||||||
|
created: 0,
|
||||||
|
updated: 0,
|
||||||
|
skipped: 0
|
||||||
|
});
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
||||||
<p className="text-muted-foreground text-base leading-relaxed mt-2">
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-2">
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
||||||
<CardHeader className="bg-muted/50 pb-4">
|
<CardHeader className="bg-muted/50 pb-4">
|
||||||
@@ -58,7 +301,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<div className="flex flex-col space-y-3 pt-2">
|
<div className="flex flex-col space-y-3 pt-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{t('systemVersion')}</span>
|
<span className="text-muted-foreground">{t('systemVersion')}</span>
|
||||||
<span className="text-foreground font-medium">{version}</span>
|
<span className="text-foreground font-medium">{t('version')} 1.5.1</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-1" />
|
<Separator className="my-1" />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@@ -68,22 +311,20 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<Separator className="my-1" />
|
<Separator className="my-1" />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
||||||
<span className="text-foreground font-medium">{releaseDate}</span>
|
<span className="text-foreground font-medium">Auguest 21, 2025</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
||||||
<CardHeader className="bg-muted/50 pb-4">
|
<CardHeader className="bg-muted/50 pb-4">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
||||||
<span>{t('links')}</span>
|
<span>{t('links')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="font-medium text-base">
|
<CardDescription className="font-medium text-base">{systemName || 'CheckCle'} {t('resources').toLowerCase()}</CardDescription>
|
||||||
{systemName || 'CheckCle'} {t('resources').toLowerCase()}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 pt-6">
|
<CardContent className="space-y-4 pt-6">
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
@@ -107,8 +348,90 @@ export const AboutSystem: React.FC = () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
||||||
|
<CardHeader className="bg-muted/50 pb-4">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className={`h-5 w-5 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'}`} />
|
||||||
|
<span>{t('updateSchema')}</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="font-medium text-base">{t('updateSchemaDesc')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 pt-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="merge-fields"
|
||||||
|
checked={mergeFields}
|
||||||
|
onCheckedChange={(checked) => setMergeFields(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="merge-fields" className="text-sm font-medium">
|
||||||
|
{t('mergeFieldsLabel')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
onClick={handleLoadLocalSchema}
|
||||||
|
disabled={isImporting}
|
||||||
|
className={`flex items-center gap-2 transition-all duration-200 ${
|
||||||
|
isImporting ? 'animate-pulse' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Database className={`h-4 w-4 ${isImporting ? 'animate-spin' : ''}`} />
|
||||||
|
{isImporting ? t('importing') : t('clickToUpdateSchema')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{importResult && (
|
||||||
|
<div className={`mt-4 p-4 rounded-lg border ${
|
||||||
|
importResult.success
|
||||||
|
? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800'
|
||||||
|
: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
|
||||||
|
} animate-in fade-in-0 slide-in-from-bottom-2 duration-300`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className={`h-5 w-5 ${
|
||||||
|
importResult.success
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-red-600 dark:text-red-400'
|
||||||
|
}`} />
|
||||||
|
<span className={`font-medium ${
|
||||||
|
importResult.success
|
||||||
|
? 'text-green-800 dark:text-green-200'
|
||||||
|
: 'text-red-800 dark:text-red-200'
|
||||||
|
}`}>
|
||||||
|
{importResult.success ? t('importSuccessful') : t('importFailed')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={`mt-2 text-sm ${
|
||||||
|
importResult.success
|
||||||
|
? 'text-green-700 dark:text-green-300'
|
||||||
|
: 'text-red-700 dark:text-red-300'
|
||||||
|
}`}>
|
||||||
|
{importResult.created > 0 && t('collectionsCreatedCount').replace('{count}', String(importResult.created))}
|
||||||
|
{importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + t('collectionsUpdatedCount').replace('{count}', String(importResult.updated))}
|
||||||
|
{importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + t('collectionsSkippedCount').replace('{count}', String(importResult.skipped))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<p className="mb-2">
|
||||||
|
<strong>{t('instructions')}:</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||||
|
<li><strong>{t('mergeFields')}:</strong> {t('instructionsMergeFields')}</li>
|
||||||
|
<li>{t('instructionsCollections')}</li>
|
||||||
|
<li>{t('instructionsImportAuth')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AboutSystem;
|
export default AboutSystem;
|
||||||
@@ -16,6 +16,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
|
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
interface TemplateDialogProps {
|
interface TemplateDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -81,6 +82,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderPlaceholderGuide = () => {
|
const renderPlaceholderGuide = () => {
|
||||||
|
|
||||||
const config = templateTypeConfigs[selectedTemplateType];
|
const config = templateTypeConfigs[selectedTemplateType];
|
||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const DataRetentionSettings = () => {
|
|||||||
setLastCleanup(result.lastCleanup);
|
setLastCleanup(result.lastCleanup);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading retention settings:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to load retention settings",
|
description: "Failed to load retention settings",
|
||||||
@@ -72,7 +71,6 @@ const DataRetentionSettings = () => {
|
|||||||
description: "Data retention settings have been updated",
|
description: "Data retention settings have been updated",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving retention settings:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to save retention settings",
|
description: "Failed to save retention settings",
|
||||||
@@ -96,7 +94,6 @@ const DataRetentionSettings = () => {
|
|||||||
// Reload settings to get updated last cleanup time
|
// Reload settings to get updated last cleanup time
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during uptime cleanup:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to perform uptime data cleanup",
|
description: "Failed to perform uptime data cleanup",
|
||||||
@@ -120,7 +117,6 @@ const DataRetentionSettings = () => {
|
|||||||
// Reload settings to get updated last cleanup time
|
// Reload settings to get updated last cleanup time
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during server cleanup:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to perform server data cleanup",
|
description: "Failed to perform server data cleanup",
|
||||||
@@ -144,7 +140,6 @@ const DataRetentionSettings = () => {
|
|||||||
// Reload settings to get updated last cleanup time
|
// Reload settings to get updated last cleanup time
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during manual cleanup:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to perform database cleanup",
|
description: "Failed to perform database cleanup",
|
||||||
@@ -210,10 +205,16 @@ const DataRetentionSettings = () => {
|
|||||||
min="1"
|
min="1"
|
||||||
max="365"
|
max="365"
|
||||||
value={settings.uptimeRetentionDays}
|
value={settings.uptimeRetentionDays}
|
||||||
onChange={(e) => setSettings(prev => ({
|
onChange={(e) => {
|
||||||
...prev,
|
const value = e.target.value;
|
||||||
uptimeRetentionDays: parseInt(e.target.value) || 30
|
if (value === '' || isNaN(Number(value))) {
|
||||||
}))}
|
return;
|
||||||
|
}
|
||||||
|
setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
uptimeRetentionDays: Number(value)
|
||||||
|
}));
|
||||||
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
@@ -229,10 +230,16 @@ const DataRetentionSettings = () => {
|
|||||||
min="1"
|
min="1"
|
||||||
max="365"
|
max="365"
|
||||||
value={settings.serverRetentionDays}
|
value={settings.serverRetentionDays}
|
||||||
onChange={(e) => setSettings(prev => ({
|
onChange={(e) => {
|
||||||
...prev,
|
const value = e.target.value;
|
||||||
serverRetentionDays: parseInt(e.target.value) || 30
|
if (value === '' || isNaN(Number(value))) {
|
||||||
}))}
|
return;
|
||||||
|
}
|
||||||
|
setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
serverRetentionDays: Number(value)
|
||||||
|
}));
|
||||||
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
@@ -250,63 +257,17 @@ const DataRetentionSettings = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col gap-4">
|
<CardFooter className="flex justify-end">
|
||||||
<div className="flex flex-wrap gap-2 w-full">
|
<Button
|
||||||
<Button
|
onClick={handleSave}
|
||||||
variant="outline"
|
disabled={isSaving}
|
||||||
onClick={handleUptimeShrink}
|
className="flex items-center gap-2"
|
||||||
disabled={isUptimeShrinking}
|
>
|
||||||
className="flex items-center gap-2"
|
{isSaving ? (
|
||||||
>
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
{isUptimeShrinking ? (
|
) : null}
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
Save Changes
|
||||||
) : (
|
</Button>
|
||||||
<Globe className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Shrink Uptime Data
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleServerShrink}
|
|
||||||
disabled={isServerShrinking}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isServerShrinking ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Server className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Shrink Server Data
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleFullShrink}
|
|
||||||
disabled={isFullShrinking}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isFullShrinking ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Full Database Shrink
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end w-full">
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isSaving}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : null}
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
|
|||||||
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||||
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
||||||
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.
|
<span className="font-medium">{t("permissionNotice")}</span> {t("permissionNoticeAddUser")}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -153,11 +153,11 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="p-4">Loading settings...</div>;
|
return <div className="p-4">{t("loadingSettings")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div className="p-4 text-red-500">Error loading settings</div>;
|
return <div className="p-4 text-red-500">{t("loadingSettingsError")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -126,15 +126,6 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
<Mail className="h-5 w-5" />
|
<Mail className="h-5 w-5" />
|
||||||
{t("sendTestEmail", "settings")}
|
{t("sendTestEmail", "settings")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-4 top-4"
|
|
||||||
onClick={handleClose}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
@@ -203,9 +194,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
{/* Info message */}
|
{/* Info message */}
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>{t("testEmailAlert", "settings")}</AlertDescription>
|
||||||
This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -223,7 +212,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<Mail className="h-4 w-4" />
|
<Mail className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{isLoading ? t("sending", "settings") : t("send", "common")}
|
{isLoading ? t("sending", "settings") : t("send", "settings")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
+205
-2
@@ -27,6 +27,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { Loader2, Copy } from "lucide-react";
|
import { Loader2, Copy } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
interface NotificationChannelDialogProps {
|
interface NotificationChannelDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -34,9 +35,10 @@ interface NotificationChannelDialogProps {
|
|||||||
editingConfig: AlertConfiguration | null;
|
editingConfig: AlertConfiguration | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const baseSchema = z.object({
|
const baseSchema = z.object({
|
||||||
notify_name: z.string().min(1, "Name is required"),
|
notify_name: z.string().min(1, "Name is required"),
|
||||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]),
|
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "notifiarr", "gotify", "webhook"]),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
service_id: z.string().default("global"),
|
service_id: z.string().default("global"),
|
||||||
template_id: z.string().optional(),
|
template_id: z.string().optional(),
|
||||||
@@ -78,12 +80,36 @@ const emailSchema = baseSchema.extend({
|
|||||||
smtp_password: z.string().min(1, "SMTP password is required"),
|
smtp_password: z.string().min(1, "SMTP password is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ntfySchema = baseSchema.extend({
|
||||||
|
notification_type: z.literal("ntfy"),
|
||||||
|
ntfy_endpoint: z.string().url("Must be a valid NTFY endpoint URL"),
|
||||||
|
api_token: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushoverSchema = baseSchema.extend({
|
||||||
|
notification_type: z.literal("pushover"),
|
||||||
|
api_token: z.string().min(1, "API token is required"),
|
||||||
|
user_key: z.string().min(1, "User key is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifiarrSchema = baseSchema.extend({
|
||||||
|
notification_type: z.literal("notifiarr"),
|
||||||
|
api_token: z.string().min(1, "API token is required"),
|
||||||
|
channel_id: z.string().min(1, "Channel ID is required"),
|
||||||
|
});
|
||||||
|
|
||||||
const webhookSchema = baseSchema.extend({
|
const webhookSchema = baseSchema.extend({
|
||||||
notification_type: z.literal("webhook"),
|
notification_type: z.literal("webhook"),
|
||||||
webhook_url: z.string().url("Must be a valid URL"),
|
webhook_url: z.string().url("Must be a valid URL"),
|
||||||
webhook_payload_template: z.string().optional(),
|
webhook_payload_template: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gotifySchema = baseSchema.extend({
|
||||||
|
notification_type: z.literal("gotify"),
|
||||||
|
api_token: z.string().min(1, "API token is required"),
|
||||||
|
server_url: z.string().url("Must be a valid server URL"),
|
||||||
|
});
|
||||||
|
|
||||||
const formSchema = z.discriminatedUnion("notification_type", [
|
const formSchema = z.discriminatedUnion("notification_type", [
|
||||||
telegramSchema,
|
telegramSchema,
|
||||||
discordSchema,
|
discordSchema,
|
||||||
@@ -91,7 +117,11 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
|||||||
signalSchema,
|
signalSchema,
|
||||||
googleChatSchema,
|
googleChatSchema,
|
||||||
emailSchema,
|
emailSchema,
|
||||||
webhookSchema
|
ntfySchema,
|
||||||
|
pushoverSchema,
|
||||||
|
notifiarrSchema,
|
||||||
|
gotifySchema,
|
||||||
|
webhookSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
@@ -133,6 +163,31 @@ const notificationTypeOptions = [
|
|||||||
description: "Send notifications via email",
|
description: "Send notifications via email",
|
||||||
icon: "/upload/notification/email.png"
|
icon: "/upload/notification/email.png"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "ntfy",
|
||||||
|
label: "NTFY",
|
||||||
|
description: "Send notifications via NTFY push service",
|
||||||
|
icon: "/upload/notification/ntfy.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "pushover",
|
||||||
|
label: "Pushover",
|
||||||
|
description: "Send push notifications via Pushover",
|
||||||
|
icon: "/upload/notification/pushover.png"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
value: "notifiarr",
|
||||||
|
label: "Notifiarr",
|
||||||
|
description: "Send notifications via Notifiarr",
|
||||||
|
icon: "/upload/notification/notifiarr.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "gotify",
|
||||||
|
label: "Gotify",
|
||||||
|
description: "Send push notifications via Gotify",
|
||||||
|
icon: "/upload/notification/gotify.png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "webhook",
|
value: "webhook",
|
||||||
label: "Webhook",
|
label: "Webhook",
|
||||||
@@ -540,6 +595,154 @@ export const NotificationChannelDialog = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{notificationType === "ntfy" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ntfy_endpoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>NTFY Endpoint</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Enter NTFY API token" {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Optional API token for authentication with NTFY server
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "pushover" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Pushover API token" {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Pushover application API token
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="user_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>User Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Pushover user key" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Pushover user key (or group key)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "notifiarr" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Notifiarr API token" {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Notifiarr API token for sending notifications
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="channel_id"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Channel ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Discord Channel ID" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The Discord channel ID where notifications will be sent
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationType === "gotify" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Gotify API token" {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Gotify application API token
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="server_url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://your-gotify-server.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The URL of your Gotify server
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{notificationType === "webhook" && (
|
{notificationType === "webhook" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const NotificationChannelList = ({
|
|||||||
case "signal": return "Signal";
|
case "signal": return "Signal";
|
||||||
case "google_chat": return "Google Chat";
|
case "google_chat": return "Google Chat";
|
||||||
case "email": return "Email";
|
case "email": return "Email";
|
||||||
|
case "pushover": return "Pushover";
|
||||||
|
case "notifiarr": return "Notifiarr";
|
||||||
case "webhook": return "Webhook";
|
case "webhook": return "Webhook";
|
||||||
default: return type || "Unknown";
|
default: return type || "Unknown";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { UserCog, Loader2, AlertCircle, Info, Users, ShieldAlert } from "lucide-
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { authService } from "@/services/authService";
|
import { authService } from "@/services/authService";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
const UserManagement = () => {
|
const UserManagement = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
const {
|
const {
|
||||||
users,
|
users,
|
||||||
loading,
|
loading,
|
||||||
@@ -44,21 +47,21 @@ const UserManagement = () => {
|
|||||||
<AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full">
|
<AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<UserCog className="h-5 w-5 mr-2 text-green-500" />
|
<UserCog className="h-5 w-5 mr-2 text-green-500" />
|
||||||
<span>User Management</span>
|
<span>{t("userManagement")}</span>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="p-4 pt-6 bg-background rounded-b-lg">
|
<AccordionContent className="p-4 pt-6 bg-background rounded-b-lg">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-2xl font-bold">User Management</h2>
|
<h2 className="text-2xl font-bold">{t("userManagement")}</h2>
|
||||||
{isSuperAdmin && <button onClick={() => setIsAddUserDialogOpen(true)} className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center">
|
{isSuperAdmin && <button onClick={() => setIsAddUserDialogOpen(true)} className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center">
|
||||||
<span className="mr-1">+</span> Add User
|
<span className="mr-1">+</span> {t("addUser")}
|
||||||
</button>}
|
</button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isSuperAdmin && <Alert className="mb-6 border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
{!isSuperAdmin && <Alert className="mb-6 border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||||
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
||||||
<span className="font-medium">Permission Notice:</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
|
<span className="font-medium">{t("permissionNotice")}</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>}
|
</Alert>}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export const SSLCertificatesTable = () => {
|
|||||||
warning_threshold: updatedCertificate.warning_threshold,
|
warning_threshold: updatedCertificate.warning_threshold,
|
||||||
expiry_threshold: updatedCertificate.expiry_threshold,
|
expiry_threshold: updatedCertificate.expiry_threshold,
|
||||||
notification_channel: updatedCertificate.notification_channel,
|
notification_channel: updatedCertificate.notification_channel,
|
||||||
|
notification_id: updatedCertificate.notification_id,
|
||||||
|
template_id: updatedCertificate.template_id,
|
||||||
check_interval: updatedCertificate.check_interval,
|
check_interval: updatedCertificate.check_interval,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import React, { createContext, useContext, useState, ReactNode } from "react";
|
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
|
||||||
import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
|
import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
|
||||||
|
|
||||||
|
type TranslationVars = Record<string, string | number>;
|
||||||
|
|
||||||
type LanguageContextType = {
|
type LanguageContextType = {
|
||||||
language: Language;
|
language: Language;
|
||||||
setLanguage: (language: Language) => void;
|
setLanguage: (language: Language) => void;
|
||||||
t: <M extends TranslationModule>(key: string, module?: M) => string;
|
t: {
|
||||||
|
(key: string): string;
|
||||||
|
(key: string, vars: TranslationVars): string;
|
||||||
|
<M extends TranslationModule>(key: string, module: M): string;
|
||||||
|
<M extends TranslationModule>(key: string, module: M, vars: TranslationVars): string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❗ Create the context with `undefined` to enforce provider usage
|
|
||||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||||
|
|
||||||
// ✅ Stable custom hook
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useLanguage = () => {
|
export const useLanguage = () => {
|
||||||
const context = useContext(LanguageContext);
|
const context = useContext(LanguageContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -21,27 +27,82 @@ export const useLanguage = () => {
|
|||||||
|
|
||||||
export const LanguageProvider = ({ children }: { children: ReactNode }) => {
|
export const LanguageProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [language, setLanguage] = useState<Language>("en");
|
const [language, setLanguage] = useState<Language>("en");
|
||||||
|
const fallbackLanguage: Language = "en";
|
||||||
|
|
||||||
const t = <M extends TranslationModule>(key: string, module?: M): string => {
|
const interpolate = (template: string, vars: TranslationVars): string => {
|
||||||
if (module) {
|
return template.replace(/{(\w+)}/g, (match, key) => {
|
||||||
const translatedValue = translations[language][module][key as TranslationKey<M>];
|
return vars[key] !== undefined ? String(vars[key]) : match;
|
||||||
return typeof translatedValue === "string" ? translatedValue : key;
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = useCallback(((
|
||||||
|
key: string,
|
||||||
|
moduleOrVars?: TranslationModule | TranslationVars,
|
||||||
|
vars?: TranslationVars
|
||||||
|
): string => {
|
||||||
|
let module: TranslationModule | undefined;
|
||||||
|
let variables: TranslationVars | undefined;
|
||||||
|
|
||||||
|
if (typeof moduleOrVars === "string") {
|
||||||
|
module = moduleOrVars;
|
||||||
|
variables = vars;
|
||||||
|
} else if (typeof moduleOrVars === "object") {
|
||||||
|
variables = moduleOrVars;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const mod in translations[language]) {
|
const langPack = translations[language];
|
||||||
const moduleKey = mod as TranslationModule;
|
const fallbackPack = translations[fallbackLanguage];
|
||||||
const translatedValue = translations[language][moduleKey][key as any];
|
let translatedText = "";
|
||||||
if (translatedValue && typeof translatedValue === "string") {
|
|
||||||
return translatedValue;
|
if (module) {
|
||||||
|
const moduleKey = key as TranslationKey<typeof module>;
|
||||||
|
const valCur = langPack?.[module]?.[moduleKey];
|
||||||
|
if (typeof valCur === "string") {
|
||||||
|
translatedText = valCur;
|
||||||
|
} else {
|
||||||
|
const valEn = fallbackPack?.[module]?.[moduleKey];
|
||||||
|
translatedText = typeof valEn === "string" ? valEn : key;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (const mod of Object.keys(langPack) as TranslationModule[]) {
|
||||||
|
const moduleTranslations = langPack[mod];
|
||||||
|
if (moduleTranslations && key in moduleTranslations) {
|
||||||
|
const val = moduleTranslations[key as keyof typeof moduleTranslations];
|
||||||
|
if (typeof val === "string") {
|
||||||
|
translatedText = val;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
for (const mod of Object.keys(fallbackPack) as TranslationModule[]) {
|
||||||
|
const moduleTranslations = fallbackPack[mod];
|
||||||
|
if (moduleTranslations && key in moduleTranslations) {
|
||||||
|
const val = moduleTranslations[key as keyof typeof moduleTranslations];
|
||||||
|
if (typeof val === "string") {
|
||||||
|
translatedText = val;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
translatedText = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return key;
|
return variables ? interpolate(translatedText, variables) : translatedText;
|
||||||
};
|
}) as LanguageContextType['t'], [language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
||||||
{children}
|
{children}
|
||||||
</LanguageContext.Provider>
|
</LanguageContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -10,8 +10,11 @@ import { User } from "@/services/userService";
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useSidebar } from "@/contexts/SidebarContext";
|
import { useSidebar } from "@/contexts/SidebarContext";
|
||||||
|
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
|
const { t } = useLanguage()
|
||||||
|
|
||||||
// Use shared sidebar state
|
// Use shared sidebar state
|
||||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
@@ -84,12 +87,12 @@ const Profile = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p>Please login to view your profile</p>
|
<p>{t("loginToViewProfile")}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/login")}
|
onClick={() => navigate("/login")}
|
||||||
className="mt-4 px-4 py-2 bg-primary text-white rounded"
|
className="mt-4 px-4 py-2 bg-primary text-white rounded"
|
||||||
>
|
>
|
||||||
Go to Login
|
{t("goToLogin")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,7 +113,7 @@ const Profile = () => {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||||
<span className="ml-2">Loading user data...</span>
|
<span className="ml-2">{t("loadingUserData")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
@@ -121,7 +124,7 @@ const Profile = () => {
|
|||||||
onClick={() => fetchUserData()}
|
onClick={() => fetchUserData()}
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
||||||
>
|
>
|
||||||
Retry
|
{t("retry")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -136,4 +139,4 @@ const Profile = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
|
|||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
collectionName?: string;
|
collectionName?: string;
|
||||||
service_id: string;
|
service_id: string;
|
||||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
|
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "notifiarr" | "gotify" | "webhook";
|
||||||
telegram_chat_id?: string;
|
telegram_chat_id?: string;
|
||||||
discord_webhook_url?: string;
|
discord_webhook_url?: string;
|
||||||
signal_number?: string;
|
signal_number?: string;
|
||||||
@@ -28,7 +28,10 @@ export interface AlertConfiguration {
|
|||||||
smtp_password?: string;
|
smtp_password?: string;
|
||||||
webhook_id?: string;
|
webhook_id?: string;
|
||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
// Webhook fields for alert_configurations
|
ntfy_endpoint?: string;
|
||||||
|
api_token?: string;
|
||||||
|
user_key?: string;
|
||||||
|
server_url?: string;
|
||||||
webhook_url?: string;
|
webhook_url?: string;
|
||||||
webhook_payload_template?: string;
|
webhook_payload_template?: string;
|
||||||
}
|
}
|
||||||
@@ -82,9 +85,24 @@ export const alertConfigService = {
|
|||||||
cleanConfig.smtp_server = config.smtp_server || "";
|
cleanConfig.smtp_server = config.smtp_server || "";
|
||||||
cleanConfig.smtp_port = config.smtp_port || "";
|
cleanConfig.smtp_port = config.smtp_port || "";
|
||||||
cleanConfig.smtp_password = config.smtp_password || "";
|
cleanConfig.smtp_password = config.smtp_password || "";
|
||||||
|
|
||||||
|
} else if (config.notification_type === "ntfy") {
|
||||||
|
cleanConfig.ntfy_endpoint = config.ntfy_endpoint || "";
|
||||||
|
cleanConfig.api_token = config.api_token || "";
|
||||||
|
|
||||||
} else if (config.notification_type === "webhook") {
|
} else if (config.notification_type === "pushover") {
|
||||||
|
cleanConfig.api_token = config.api_token || "";
|
||||||
|
cleanConfig.user_key = config.user_key || "";
|
||||||
|
|
||||||
|
} else if (config.notification_type === "notifiarr") {
|
||||||
|
cleanConfig.api_token = config.api_token || "";
|
||||||
|
cleanConfig.channel_id = config.channel_id || "";
|
||||||
|
|
||||||
|
} else if (config.notification_type === "gotify") {
|
||||||
|
cleanConfig.api_token = config.api_token || "";
|
||||||
|
cleanConfig.server_url = config.server_url || "";
|
||||||
|
|
||||||
|
} else if (config.notification_type === "webhook") {
|
||||||
cleanConfig.webhook_url = config.webhook_url || "";
|
cleanConfig.webhook_url = config.webhook_url || "";
|
||||||
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
|
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
|
|||||||
affected_systems: data.affected_systems,
|
affected_systems: data.affected_systems,
|
||||||
priority: data.priority.toLowerCase(),
|
priority: data.priority.toLowerCase(),
|
||||||
service_id: data.service_id || '',
|
service_id: data.service_id || '',
|
||||||
assigned_to: data.assigned_to || '', // Direct user ID assignment
|
//assigned_to: data.assigned_to || '', // Direct user ID assignment
|
||||||
|
//2025-08-18: // map UI field -> server schema
|
||||||
|
assigned_users: typeof data.assigned_to === 'string' ? data.assigned_to : '',
|
||||||
root_cause: data.root_cause || '',
|
root_cause: data.root_cause || '',
|
||||||
resolution_steps: data.resolution_steps || '',
|
resolution_steps: data.resolution_steps || '',
|
||||||
lessons_learned: data.lessons_learned || '',
|
lessons_learned: data.lessons_learned || '',
|
||||||
@@ -82,28 +84,41 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
|
|||||||
export const updateIncident = async (id: string, data: Partial<IncidentItem>): Promise<void> => {
|
export const updateIncident = async (id: string, data: Partial<IncidentItem>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
console.log(`Updating incident ${id} with:`, data);
|
console.log(`Updating incident ${id} with:`, data);
|
||||||
|
|
||||||
// Make sure impact and priority are lowercase
|
const payload: Record<string, any> = {
|
||||||
const payload = {
|
|
||||||
...data,
|
...data,
|
||||||
impact: data.impact?.toLowerCase(),
|
...(data.impact ? { impact: data.impact.toLowerCase() } : {}),
|
||||||
priority: data.priority?.toLowerCase(),
|
...(data.priority ? { priority: data.priority.toLowerCase() } : {}),
|
||||||
status: data.status ? formatStatus(data.status) : undefined,
|
...(data.status
|
||||||
impact_status: data.status ? data.status.toLowerCase() : undefined,
|
? {
|
||||||
...(data.status?.toLowerCase() === 'resolved' && !data.resolution_time
|
status: formatStatus(data.status),
|
||||||
? { resolution_time: new Date().toISOString() }
|
impact_status: data.status.toLowerCase(),
|
||||||
: {})
|
}
|
||||||
|
: {}),
|
||||||
|
// set only if assigned_to was provided
|
||||||
|
...(typeof data.assigned_to === 'string'
|
||||||
|
? {
|
||||||
|
assigned_users: data.assigned_to, // server field
|
||||||
|
assigned_to: data.assigned_to, // legacy field for backward compatibility
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
// add resolution_time only if status changes to resolved
|
||||||
|
...((data.status?.toLowerCase() === 'resolved' && !data.resolution_time)
|
||||||
|
? { resolution_time: new Date().toISOString() }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Final payload for update:", payload);
|
console.log("Final payload for update:", payload);
|
||||||
await pb.collection('incidents').update(id, payload);
|
await pb.collection('incidents').update(id, payload);
|
||||||
|
|
||||||
// Invalidate cache after update
|
// Invalidate cache after update
|
||||||
invalidateCache();
|
invalidateCache();
|
||||||
|
|
||||||
console.log(`Incident ${id} updated successfully`);
|
console.log(`Incident ${id} updated successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating incident:', error);
|
console.error('Error updating incident:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import { IncidentItem } from '../types';
|
import { IncidentItem } from '../types';
|
||||||
|
import { userService } from '@/services/userService';
|
||||||
import {
|
import {
|
||||||
addBasicInfoSection,
|
addBasicInfoSection,
|
||||||
addDescriptionSection,
|
addDescriptionSection,
|
||||||
@@ -22,6 +23,18 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
|
|||||||
throw new Error('Invalid incident data');
|
throw new Error('Invalid incident data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch assigned user data if available
|
||||||
|
let assignedUser: { full_name?: string; username?: string } | null = null;
|
||||||
|
const assigneeId = incident?.assigned_users || incident?.assigned_to;
|
||||||
|
if (assigneeId) {
|
||||||
|
try {
|
||||||
|
assignedUser = await userService.getUser(assigneeId);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch assigned user for PDF:', error);
|
||||||
|
// Continue without user data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create new PDF document with portrait orientation
|
// Create new PDF document with portrait orientation
|
||||||
const doc = new jsPDF({
|
const doc = new jsPDF({
|
||||||
@@ -85,7 +98,7 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add assignment section
|
// Add assignment section
|
||||||
yPos = addAssignmentSection(doc, incident, yPos);
|
yPos = addAssignmentSection(doc, incident, yPos, assignedUser);
|
||||||
|
|
||||||
// Check if we need to add a new page
|
// Check if we need to add a new page
|
||||||
if (yPos > 250) {
|
if (yPos > 250) {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const addResolutionSection = (doc: jsPDF, incident: IncidentItem, yPos: n
|
|||||||
/**
|
/**
|
||||||
* Add the assignment information section to the PDF
|
* Add the assignment information section to the PDF
|
||||||
*/
|
*/
|
||||||
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number, assignedUser?: { full_name?: string; username?: string } | null): number => {
|
||||||
doc.setFontSize(14);
|
doc.setFontSize(14);
|
||||||
doc.setTextColor(30, 64, 175); // Blue-800
|
doc.setTextColor(30, 64, 175); // Blue-800
|
||||||
doc.text('Assignment Information', 15, yPos);
|
doc.text('Assignment Information', 15, yPos);
|
||||||
@@ -158,8 +158,16 @@ export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: n
|
|||||||
yPos += 10;
|
yPos += 10;
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setTextColor(0, 0, 0);
|
doc.setTextColor(0, 0, 0);
|
||||||
const assignedTo = incident.assigned_to || 'Not assigned';
|
|
||||||
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
|
if (assignedUser) {
|
||||||
|
// Show user name if available
|
||||||
|
const userName = assignedUser.full_name || assignedUser.username || 'Unknown User';
|
||||||
|
doc.text(`Assigned to: ${userName}`, 15, yPos);
|
||||||
|
} else {
|
||||||
|
// Fallback to ID if no user data available
|
||||||
|
const assignedTo = incident.assigned_users || incident.assigned_to || 'Not assigned';
|
||||||
|
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
|
||||||
|
}
|
||||||
|
|
||||||
return yPos + 10;
|
return yPos + 10;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export type IncidentItem = {
|
|||||||
service_id?: string;
|
service_id?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
description: string;
|
description: string;
|
||||||
assigned_to?: string;
|
assigned_to?: string; // legacy UI field
|
||||||
|
assigned_users?: string; // server field
|
||||||
resolution_time?: string;
|
resolution_time?: string;
|
||||||
impact: string;
|
impact: string;
|
||||||
affected_systems: string;
|
affected_systems: string;
|
||||||
@@ -32,7 +33,8 @@ export type CreateIncidentInput = {
|
|||||||
affected_systems: string;
|
affected_systems: string;
|
||||||
priority: string;
|
priority: string;
|
||||||
service_id?: string;
|
service_id?: string;
|
||||||
assigned_to?: string;
|
assigned_to?: string; // legacy UI field
|
||||||
|
assigned_users?: string; // server field
|
||||||
root_cause?: string;
|
root_cause?: string;
|
||||||
resolution_steps?: string;
|
resolution_steps?: string;
|
||||||
lessons_learned?: string;
|
lessons_learned?: string;
|
||||||
@@ -50,7 +52,8 @@ export type UpdateIncidentInput = {
|
|||||||
affected_systems?: string;
|
affected_systems?: string;
|
||||||
priority?: string;
|
priority?: string;
|
||||||
service_id?: string;
|
service_id?: string;
|
||||||
assigned_to?: string;
|
assigned_to?: string; // legacy UI field
|
||||||
|
assigned_users?: string; // server field
|
||||||
root_cause?: string;
|
root_cause?: string;
|
||||||
resolution_steps?: string;
|
resolution_steps?: string;
|
||||||
lessons_learned?: string;
|
lessons_learned?: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AboutTranslations } from '../types/about';
|
import { AboutTranslations } from '../types/about';
|
||||||
|
|
||||||
export const aboutTranslations: AboutTranslations = {
|
export const aboutTranslations: AboutTranslations = {
|
||||||
aboutCheckcle: "Über Checkcle",
|
aboutCheckCle: "Über Checkcle",
|
||||||
systemDescription: "Checkcle ist ein Open-Source-Überwachungs-Stack, der Echtzeit-Einblicke in Server- und Dienstzustände, Vorfallmanagement und operative Transparenz bietet. Veröffentlicht unter der MIT-Lizenz.",
|
systemDescription: "Checkcle ist ein Open-Source-Überwachungs-Stack, der Echtzeit-Einblicke in Server- und Dienstzustände, Vorfallmanagement und operative Transparenz bietet. Veröffentlicht unter der MIT-Lizenz.",
|
||||||
systemVersion: "Systemversion",
|
systemVersion: "Systemversion",
|
||||||
license: "Lizenz",
|
license: "Lizenz",
|
||||||
@@ -15,6 +15,7 @@ export const aboutTranslations: AboutTranslations = {
|
|||||||
quickActions: "Schnelle Aktionen",
|
quickActions: "Schnelle Aktionen",
|
||||||
quickActionsDescription: "Greifen Sie schnell auf gängige Überwachungsvorgänge und -funktionen zu. Wählen Sie unten eine Aktion aus, um loszulegen.",
|
quickActionsDescription: "Greifen Sie schnell auf gängige Überwachungsvorgänge und -funktionen zu. Wählen Sie unten eine Aktion aus, um loszulegen.",
|
||||||
quickTips: "Schnelle Tipps",
|
quickTips: "Schnelle Tipps",
|
||||||
|
releasedOn: "Veröffentlicht",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
logout: "Abmelden",
|
logout: "Abmelden",
|
||||||
language: "Sprache",
|
language: "Sprache",
|
||||||
english: "Englisch",
|
english: "Englisch",
|
||||||
khmer: "Khmer",
|
khmer: "ភាសាខ្មែរ",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
simplifiedChinese: "Simplified Chinese",
|
korean: "한국어",
|
||||||
|
japanese: "日本語",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
goodMorning: "Guten Morgen",
|
goodMorning: "Guten Morgen",
|
||||||
goodAfternoon: "Guten Nachmittag",
|
goodAfternoon: "Guten Nachmittag",
|
||||||
goodEvening: "Guten Abend",
|
goodEvening: "Guten Abend",
|
||||||
|
|||||||
@@ -51,4 +51,13 @@ export const incidentTranslations: IncidentTranslations = {
|
|||||||
configuration: 'Konfiguration',
|
configuration: 'Konfiguration',
|
||||||
failedToUpdateStatus: 'Fehler beim Aktualisieren des Status',
|
failedToUpdateStatus: 'Fehler beim Aktualisieren des Status',
|
||||||
inProgress: 'In Bearbeitung',
|
inProgress: 'In Bearbeitung',
|
||||||
|
enterRootCause: 'Grundursache eingeben',
|
||||||
|
enterIncidentTitle: 'Vorfalltitel eingeben',
|
||||||
|
enterIncidentDescription: 'Vorfallbeschreibung eingeben',
|
||||||
|
enterServiceId: 'Service-IDs durch Komma getrennt eingeben',
|
||||||
|
selectAssignedUser: 'Zugewiesenen Benutzer auswählen',
|
||||||
|
noAssignedUser: 'Der Vorfall ist derzeit keinem Benutzer zugewiesen',
|
||||||
|
affectedSystems: 'Betroffene Systeme',
|
||||||
|
enterAffectedSystems: 'Betroffene Systeme eingeben',
|
||||||
|
separateSystemsWithComma: 'Betroffene Systeme durch Komma getrennt eingeben',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,4 +64,17 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
|||||||
noScheduledMaintenance: 'Keine geplanten Wartungen',
|
noScheduledMaintenance: 'Keine geplanten Wartungen',
|
||||||
noMaintenanceWindows: 'Für diesen Zeitraum sind keine Wartungsfenster vorhanden. Erstellen Sie eines über die Schaltfläche „Wartung erstellen“.',
|
noMaintenanceWindows: 'Für diesen Zeitraum sind keine Wartungsfenster vorhanden. Erstellen Sie eines über die Schaltfläche „Wartung erstellen“.',
|
||||||
maintenanceCreatedSuccess: 'Wartungsfenster erfolgreich erstellt',
|
maintenanceCreatedSuccess: 'Wartungsfenster erfolgreich erstellt',
|
||||||
|
editMaintenanceWindow: 'Wartung bearbeiten',
|
||||||
|
editMaintenanceDesc: 'Wartungsdetails bearbeiten',
|
||||||
|
priorityDescription: 'Wartungspriorität auswählen',
|
||||||
|
statusDescription: 'Wartungsstatus auswählen',
|
||||||
|
impactLevel: 'Auswirkungsgrad',
|
||||||
|
impactLevelDescription: 'Auswirkungsgrad auswählen',
|
||||||
|
selectAssignedUsers: 'Zugewiesene Benutzer auswählen',
|
||||||
|
assignedPersonnelDescription: 'Derzeit zugewiesene Benutzer',
|
||||||
|
mutedNotifications: 'Benachrichtigungen deaktiviert',
|
||||||
|
notificationsAreMuted: 'Benachrichtigungen sind für diese Wartung derzeit deaktiviert',
|
||||||
|
selectNotificationChannel: 'Benachrichtigungskanal hinzufügen',
|
||||||
|
enableNotificationsFirst: 'Aktivieren Sie zuerst Benachrichtigungen, um einen Kanal auszuwählen',
|
||||||
|
updateMaintenance: 'Wartung aktualisieren',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,98 @@
|
|||||||
|
|
||||||
import { ServicesTranslations } from '../types/services';
|
import { ServicesTranslations } from '../types/services';
|
||||||
|
|
||||||
export const servicesTranslations: ServicesTranslations = {
|
export const servicesTranslations: ServicesTranslations = {
|
||||||
// Dienst-Tabelle
|
// Service Table
|
||||||
serviceName: "Dienstname",
|
|
||||||
serviceType: "Diensttyp",
|
|
||||||
serviceStatus: "Dienststatus",
|
serviceStatus: "Dienststatus",
|
||||||
responseTime: "Antwortzeit",
|
responseTime: "Antwortzeit",
|
||||||
uptime: "Betriebszeit",
|
uptime: "Betriebszeit",
|
||||||
lastChecked: "Zuletzt überprüft",
|
lastChecked: "Zuletzt überprüft",
|
||||||
noServices: "Keine Dienste entsprechen Ihren Filterkriterien.",
|
noServices: "Keine Dienste entsprechen Ihren Filterkriterien.",
|
||||||
|
currentlyMonitoring: "Derzeit überwacht",
|
||||||
|
retry: "Wiederholen",
|
||||||
|
overview: "Überblick",
|
||||||
|
newService: "Neuer Dienst",
|
||||||
|
rowsPerPage: "Zeilen pro Seite",
|
||||||
|
search: "Suchen",
|
||||||
|
allTypes: "Alle Arten",
|
||||||
|
createNewService: "Neuen Service hinzufügen",
|
||||||
|
createNewServiceDesc: "Geben Sie detaillierte Informationen ein, um einen neuen zu erstellen, den Sie überwachen möchten.",
|
||||||
|
|
||||||
|
// ServiceBasicFields.tsx
|
||||||
|
serviceName: "Dienstname",
|
||||||
|
serviceNameDesc: "Geben Sie einen aussagekräftigen Namen für Ihren Service ein",
|
||||||
|
|
||||||
|
// ServiceConfigFields.tsx
|
||||||
|
checkInterval: "Prüfintervall",
|
||||||
|
seconds: "Sekunden",
|
||||||
|
minute: "Minute",
|
||||||
|
minutes: "Minuten",
|
||||||
|
hour: "Stunde",
|
||||||
|
hours: "Stunden",
|
||||||
|
custom: "Benutzerdefiniert",
|
||||||
|
checkIntervalPlaceholder: "Intervall in Sekunden eingeben",
|
||||||
|
backToPresets: "Zurück zu Voreinstellungen",
|
||||||
|
checkIntervalDesc: "Wie oft der Dienststatus überprüft werden soll",
|
||||||
|
checkIntervalDescCustom: "Benutzerdefiniertes Intervall in Sekunden eingeben (mindestens 10 Sekunden)",
|
||||||
|
retryAttempts: "Wiederholungsversuche",
|
||||||
|
attempt: "Versuch",
|
||||||
|
attempts: "Versuche",
|
||||||
|
retryAttemptsDesc: "Anzahl der Wiederholungsversuche, bevor der Dienst als ausgefallen markiert wird",
|
||||||
|
|
||||||
|
// ServiceForm.tsx
|
||||||
|
updateService: "Dienst aktualisieren",
|
||||||
|
createService: "Dienst erstellen",
|
||||||
|
|
||||||
|
// ServiceNotificationFields.tsx
|
||||||
|
enableNotifications: "Benachrichtigungen aktivieren",
|
||||||
|
enableNotificationsDesc: "Benachrichtigungen für diesen Dienst aktivieren oder deaktivieren",
|
||||||
|
notificationChannels: "Benachrichtigungskanäle",
|
||||||
|
notificationChannelsEnabledDesc: "Benachrichtigungskanäle für diesen Dienst auswählen",
|
||||||
|
notificationChannelsDesc: "Aktivieren Sie zuerst Benachrichtigungen, um Kanäle auszuwählen",
|
||||||
|
notificationChannelsPlaceholder: "Benachrichtigungskanal hinzufügen",
|
||||||
|
alertTemplate: "Alarmvorlage",
|
||||||
|
alertTemplateLoading: "Lade Vorlagen...",
|
||||||
|
alertTemplatePlaceholder: "Alarmvorlage auswählen",
|
||||||
|
alertTemplateEnabledDesc: "Wählen Sie eine Vorlage für Alarmmeldungen",
|
||||||
|
alertTemplateDesc: "Aktivieren Sie zuerst Benachrichtigungen, um eine Vorlage auszuwählen",
|
||||||
|
|
||||||
|
// ServiceTypeField.tsx
|
||||||
|
serviceType: "Diensttyp",
|
||||||
|
serviceTypeHTTPDesc: "Webseiten und REST-APIs mit HTTP/HTTPS-Protokoll überwachen",
|
||||||
|
serviceTypePINGDesc: "Host-Verfügbarkeit mit PING-Protokoll überwachen",
|
||||||
|
serviceTypeTCPDesc: "TCP-Port-Konnektivität mit TCP-Protokoll überwachen",
|
||||||
|
serviceTypeDNSDesc: "DNS-Auflösung überwachen",
|
||||||
|
|
||||||
|
// ServiceRegionalFields.tsx
|
||||||
|
regionalMonitoring: "Regionale Überwachung",
|
||||||
|
regionalMonitoringDesc: "Diesen Dienst regionalen Überwachungsagenten für verteilte Überwachung zuweisen",
|
||||||
|
regionalAgents: "Regionale Agenten",
|
||||||
|
regionalAgentsLoading: "Lade Agenten...",
|
||||||
|
regionalAgentsAvailablePlaceholder: "Zusätzliche regionale Agenten auswählen...",
|
||||||
|
regionalAgentsAllSelected: "Alle verfügbaren Agenten ausgewählt",
|
||||||
|
regionalAgentsNoAvailable: "Keine regionalen Agenten verfügbar",
|
||||||
|
regionalAgentsNoOnlineAvailable: "Keine regionalen Agenten online verfügbar",
|
||||||
|
regionalAgentsNotFoundMessage: "Keine regionalen Online-Agenten gefunden. Dienste verwenden die Standardüberwachung.",
|
||||||
|
regionalAgentsNotSelectedMessage: "Keine regionalen Agenten ausgewählt. Der Dienst verwendet die Standardüberwachung.",
|
||||||
|
|
||||||
|
// ServiceUrlField.tsx
|
||||||
|
targetDefault: "Ziel-URL/Host",
|
||||||
|
targetDNS: "Domainname",
|
||||||
|
targetHTTPDesc: "Geben Sie die vollständige URL inklusive Protokoll ein (http:// oder https://)",
|
||||||
|
targetPINGDesc: "Geben Sie den Hostnamen oder die IP-Adresse zum Pingen ein",
|
||||||
|
targetTCPDesc: "Geben Sie Hostname oder IP-Adresse für TCP-Verbindungstest ein",
|
||||||
|
targetTCPPortDesc: "Geben Sie die Portnummer für den TCP-Verbindungstest ein",
|
||||||
|
targetDNSDesc: "Domainnamen für DNS-Überwachung eingeben (A, AAAA, MX, etc.)",
|
||||||
|
targetDefaultDesc: "Geben Sie die Ziel-URL oder den Hostnamen für die Überwachung ein",
|
||||||
|
targetDefaultPlaceholder: "URL oder Hostname eingeben",
|
||||||
|
|
||||||
|
// types.ts
|
||||||
|
serviceNameRequired: "Dienstname ist erforderlich",
|
||||||
|
urlDomainHostRequired: "URL/Domain/Host ist erforderlich",
|
||||||
|
enterValidUrlHostnameDomain: "Bitte eine gültige URL, einen Hostnamen oder eine Domain eingeben",
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
upServices: "DIENSTE ONLINE",
|
||||||
|
downServices: "DIENSTE OFFLINE",
|
||||||
|
pausedServices: "DIENSTE PAUSIERT",
|
||||||
|
warningServices: "DIENSTE MIT WARNUNG",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const settingsTranslations: SettingsTranslations = {
|
|||||||
enableTLS: "TLS aktivieren",
|
enableTLS: "TLS aktivieren",
|
||||||
localName: "Lokaler Name",
|
localName: "Lokaler Name",
|
||||||
|
|
||||||
// Test Email
|
// Test Email
|
||||||
testEmail: "Test Email",
|
testEmail: "Test Email",
|
||||||
sendTestEmail: "Send test email",
|
sendTestEmail: "Send test email",
|
||||||
emailTemplate: "Email template",
|
emailTemplate: "Email template",
|
||||||
@@ -47,5 +47,12 @@ export const settingsTranslations: SettingsTranslations = {
|
|||||||
testConnection: "Verbindung testen",
|
testConnection: "Verbindung testen",
|
||||||
testingConnection: "Verbindung wird getestet...",
|
testingConnection: "Verbindung wird getestet...",
|
||||||
connectionSuccess: "Verbindung erfolgreich",
|
connectionSuccess: "Verbindung erfolgreich",
|
||||||
connectionFailed: "Verbindung fehlgeschlagen"
|
connectionFailed: "Verbindung fehlgeschlagen",
|
||||||
|
|
||||||
|
// Ergänzte fehlende Einträge
|
||||||
|
addUser: "Benutzer hinzufügen",
|
||||||
|
permissionNotice: "Berechtigungshinweis:",
|
||||||
|
permissionNoticeAddUser: "Als Admin-Benutzer haben Sie keinen Zugriff auf die Anzeige oder Änderung von System- und E-Mail-Einstellungen. Diese Einstellungen können nur von Super-Admins aufgerufen und geändert werden. Wenden Sie sich an Ihren Super-Admin, wenn Sie Änderungen an der Systemkonfiguration oder den E-Mail-Einstellungen vornehmen müssen.",
|
||||||
|
loadingSettings: "Einstellungen werden geladen...",
|
||||||
|
loadingSettingsError: "Fehler beim Laden der Einstellungen",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
created: "Erstellt",
|
created: "Erstellt",
|
||||||
lastUpdated: "Zuletzt aktualisiert",
|
lastUpdated: "Zuletzt aktualisiert",
|
||||||
lastNotification: "Letzte Benachrichtigung",
|
lastNotification: "Letzte Benachrichtigung",
|
||||||
collectionId: "Sammlungs-ID"
|
collectionId: "Sammlungs-ID",
|
||||||
|
noCertificatesFound: "Keine Zertifikate gefunden",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AboutTranslations } from '../types/about';
|
import { AboutTranslations } from '../types/about';
|
||||||
|
|
||||||
export const aboutTranslations: AboutTranslations = {
|
export const aboutTranslations: AboutTranslations = {
|
||||||
aboutCheckcle: "About Checkcle",
|
aboutCheckCle: "About Checkcle",
|
||||||
systemDescription: "Checkcle is an open-source monitoring stack offering real-time insights into server and service health, incident management, and operational transparency. Released as MIT License.",
|
systemDescription: "Checkcle is an open-source monitoring stack offering real-time insights into server and service health, incident management, and operational transparency. Released as MIT License.",
|
||||||
systemVersion: "System Version",
|
systemVersion: "System Version",
|
||||||
license: "License",
|
license: "License",
|
||||||
@@ -15,4 +15,20 @@ export const aboutTranslations: AboutTranslations = {
|
|||||||
quickActions: "Quick Actions",
|
quickActions: "Quick Actions",
|
||||||
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
|
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
|
||||||
quickTips: "Quick Tips",
|
quickTips: "Quick Tips",
|
||||||
|
releasedOn: "Released On",
|
||||||
|
updateSchema: "Update Schema",
|
||||||
|
updateSchemaDesc: "Automatic import collections schema",
|
||||||
|
mergeFieldsLabel: "Merge fields with existing collections (safe - preserves data)",
|
||||||
|
importing: "Importing...",
|
||||||
|
clickToUpdateSchema: "Click to update Schema",
|
||||||
|
importSuccessful: "Import Successful",
|
||||||
|
importFailed: "Import Failed",
|
||||||
|
instructions: "Instructions",
|
||||||
|
mergeFields: "Merge fields",
|
||||||
|
instructionsMergeFields: "Safely add new fields to existing collections, preserves all data",
|
||||||
|
instructionsCollections: "System collections (starting with _) and users collection will be skipped automatically",
|
||||||
|
instructionsImportAuth: "Only authenticated admins can perform schema imports",
|
||||||
|
collectionsUpdatedCount: "{count} collections updated",
|
||||||
|
collectionsCreatedCount: "{count} collections created",
|
||||||
|
collectionsSkippedCount: "{count} collections created",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
welcome: "Welcome",
|
welcome: "Welcome",
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
language: "Language",
|
language: "Language",
|
||||||
english: "English",
|
english: "Englisch",
|
||||||
khmer: "Khmer",
|
khmer: "ភាសាខ្មែរ",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
simplifiedChinese: "Simplified Chinese",
|
korean: "한국어",
|
||||||
|
japanese: "日本語",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
goodMorning: "Good morning",
|
goodMorning: "Good morning",
|
||||||
goodAfternoon: "Good afternoon",
|
goodAfternoon: "Good afternoon",
|
||||||
goodEvening: "Good evening",
|
goodEvening: "Good evening",
|
||||||
|
|||||||
@@ -52,4 +52,14 @@ export const incidentTranslations: IncidentTranslations = {
|
|||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
failedToUpdateStatus: 'Failed to update status',
|
failedToUpdateStatus: 'Failed to update status',
|
||||||
inProgress: 'In Progress',
|
inProgress: 'In Progress',
|
||||||
|
enterRootCause: 'Enter Root Cause',
|
||||||
|
enterIncidentTitle: 'Enter Incident Title',
|
||||||
|
enterIncidentDescription: 'Enter Incident Description',
|
||||||
|
enterServiceId: 'Enter Service-IDs seperated by comma',
|
||||||
|
selectAssignedUser: 'Select Assigned User',
|
||||||
|
noAssignedUser: 'Incident is currently not assigned to any user',
|
||||||
|
affectedSystems: 'Affected Systems',
|
||||||
|
enterAffectedSystems: 'Enter Affected Systems',
|
||||||
|
separateSystemsWithComma: 'Enter affected systems seperated by comma',
|
||||||
|
createIncidentDesc: 'Create a new incident',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,4 +65,25 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
|||||||
noScheduledMaintenance: 'No Scheduled Maintenance',
|
noScheduledMaintenance: 'No Scheduled Maintenance',
|
||||||
noMaintenanceWindows: 'There are no maintenance windows for this period. Create one by clicking the "Create Maintenance" button.',
|
noMaintenanceWindows: 'There are no maintenance windows for this period. Create one by clicking the "Create Maintenance" button.',
|
||||||
maintenanceCreatedSuccess: 'Maintenance window created successfully',
|
maintenanceCreatedSuccess: 'Maintenance window created successfully',
|
||||||
|
editMaintenanceWindow: 'Edit Maintenance',
|
||||||
|
editMaintenanceDesc: 'Edit maintenance details',
|
||||||
|
priorityDescription: 'Select maintenance priority',
|
||||||
|
statusDescription: 'Select maintenance status',
|
||||||
|
impactLevel: 'Impact Level',
|
||||||
|
impactLevelDescription: 'Select impact level',
|
||||||
|
selectAssignedUsers: 'Select Assigned Users',
|
||||||
|
assignedPersonnelDescription: 'Currently assigned users',
|
||||||
|
mutedNotifications: 'Notifications disabled',
|
||||||
|
notificationsAreMuted: 'Notifications are currently muted for this maintenance',
|
||||||
|
selectNotificationChannel: 'Add a notification channel',
|
||||||
|
enableNotificationsFirst: 'Enable notifications first to select channel',
|
||||||
|
updateMaintenance: 'Update Maintenance',
|
||||||
|
loginToViewProfile: 'Please login to view your profile',
|
||||||
|
goToLogin: 'Go to Login',
|
||||||
|
loadingUserData: 'Loading user data...',
|
||||||
|
retry: 'Retry',
|
||||||
|
loadingServerData: 'Loading server data...',
|
||||||
|
retrievingYourInformation: 'Please wait while we retrieve your information...',
|
||||||
|
servicesPagination: '{startItem}-{endItem} of {totalItems} services',
|
||||||
|
servicesPaginationNoService: '0 services',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,138 @@
|
|||||||
import { ServicesTranslations } from '../types/services';
|
import { ServicesTranslations } from '../types/services';
|
||||||
|
|
||||||
export const servicesTranslations: ServicesTranslations = {
|
export const servicesTranslations: ServicesTranslations = {
|
||||||
serviceName: "Service Name",
|
|
||||||
serviceType: "Service Type",
|
|
||||||
serviceStatus: "Service Status",
|
serviceStatus: "Service Status",
|
||||||
responseTime: "Response Time",
|
|
||||||
uptime: "Uptime",
|
uptime: "Uptime",
|
||||||
lastChecked: "Last Checked",
|
lastChecked: "Last Checked",
|
||||||
noServices: "No services match your filter criteria.",
|
noServices: "No services match your filter criteria.",
|
||||||
};
|
currentlyMonitoring: "Currently Monitoring",
|
||||||
|
retry: "Retry",
|
||||||
|
overview: "Overview",
|
||||||
|
newService: "NewService",
|
||||||
|
rowsPerPage: "Rows Per Page",
|
||||||
|
search: "Search",
|
||||||
|
allTypes: "All Types",
|
||||||
|
createNewService: "Create New Service",
|
||||||
|
createNewServiceDesc: "Fill in the details to create a new service to monitor.",
|
||||||
|
|
||||||
|
// ServiceBasicFields.tsx
|
||||||
|
serviceName: "Service Name",
|
||||||
|
serviceNameDesc: "Enter a descriptive name for your service",
|
||||||
|
|
||||||
|
// ServiceConfigFields.tsx
|
||||||
|
checkInterval: "Check Interval",
|
||||||
|
seconds: "seconds",
|
||||||
|
minute: "minute",
|
||||||
|
minutes: "minutes",
|
||||||
|
hour: "hour",
|
||||||
|
hours: "hours",
|
||||||
|
custom: "Custom",
|
||||||
|
checkIntervalPlaceholder: "Enter interval in seconds",
|
||||||
|
backToPresets: "Back to presets",
|
||||||
|
checkIntervalDesc: "How often to check the service status",
|
||||||
|
checkIntervalDescCustom: "Enter custom interval in seconds (minimum 10 seconds)",
|
||||||
|
retryAttempts: "Retry Attempts",
|
||||||
|
attempt: "attempt",
|
||||||
|
attempts: "attempts",
|
||||||
|
retryAttemptsDesc: "Number of retry attempts before marking as down",
|
||||||
|
|
||||||
|
// ServiceForm.tsx
|
||||||
|
updateService: "Update Service",
|
||||||
|
createService: "Create Service",
|
||||||
|
|
||||||
|
// ServiceNotificationFields.tsx
|
||||||
|
enableNotifications: "Enable Notifications",
|
||||||
|
enableNotificationsDesc: "Enable or disable notifications for this service",
|
||||||
|
notificationChannels: "Notification Channels",
|
||||||
|
notificationChannelsEnabledDesc: "Select notification channels for this service",
|
||||||
|
notificationChannelsDesc: "Enable notifications first to select channels",
|
||||||
|
notificationChannelsPlaceholder: "Add a notification channel",
|
||||||
|
alertTemplate: "Alert Template",
|
||||||
|
alertTemplateLoading: "Loading templates...",
|
||||||
|
alertTemplatePlaceholder: "Select an alert template",
|
||||||
|
alertTemplateEnabledDesc: "Choose a template for alert messages",
|
||||||
|
alertTemplateDesc: "Enable notifications first to select template",
|
||||||
|
|
||||||
|
// ServiceTypeField.tsx
|
||||||
|
serviceType: "Service Type",
|
||||||
|
serviceTypeHTTPDesc: "Monitor websites and REST APIs with HTTP/HTTPS Protocol",
|
||||||
|
serviceTypePINGDesc: "Monitor host availability with PING Protocol",
|
||||||
|
serviceTypeTCPDesc: "Monitor TCP port connectivity with TCP Protocol",
|
||||||
|
serviceTypeDNSDesc: "Monitor DNS resolution",
|
||||||
|
|
||||||
|
// ServiceRegionalFields.tsx
|
||||||
|
regionalMonitoring: "Regional Monitoring",
|
||||||
|
regionalMonitoringDesc: "Assign this service to regional monitoring agents for distributed monitoring",
|
||||||
|
regionalAgents: "Regional Agents",
|
||||||
|
regionalAgentsLoading: "Loading agents...",
|
||||||
|
regionalAgentsAvailablePlaceholder: "Select additional regional agents...",
|
||||||
|
regionalAgentsAllSelected: "All available agents selected",
|
||||||
|
regionalAgentsNoAvailable: "No regional agents available",
|
||||||
|
regionalAgentsNoOnlineAvailable: "No online regional agents available",
|
||||||
|
regionalAgentsNotFoundMessage: "No online regional agents found. Services will use default monitoring.",
|
||||||
|
regionalAgentsNotSelectedMessage: "No regional agents selected. Service will use default monitoring.",
|
||||||
|
|
||||||
|
// ServiceUrlField.tsx
|
||||||
|
targetDefault: "Target URL/Host",
|
||||||
|
targetDNS: "Domain Name",
|
||||||
|
targetHTTPDesc: "Enter the full URL including protocol (http:// or https://)",
|
||||||
|
targetPINGDesc: "Enter hostname or IP address to ping",
|
||||||
|
targetTCPDesc: "Enter hostname or IP address for TCP connection test",
|
||||||
|
targetTCPPortDesc: "Enter the port number for TCP connection test",
|
||||||
|
targetDNSDesc: "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)",
|
||||||
|
targetDefaultDesc: "Enter the target URL or hostname for monitoring",
|
||||||
|
targetDefaultPlaceholder: "Enter URL or hostname",
|
||||||
|
|
||||||
|
// types.ts
|
||||||
|
serviceNameRequired: "Service name is required",
|
||||||
|
urlDomainHostRequired: "URL/Domain/Host is required",
|
||||||
|
enterValidUrlHostnameDomain: "Please enter a valid URL, hostname, or domain",
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
upServices: "UP SERVICES",
|
||||||
|
downServices: "DOWN SERVICES",
|
||||||
|
pausedServices: "PAUSED SERVICES",
|
||||||
|
warningServices: "WARNING SERVICES",
|
||||||
|
|
||||||
|
// ServiceRowActions.tsx
|
||||||
|
viewDetail: "View Detail",
|
||||||
|
resumeMonitoring: "Resume Monitoring",
|
||||||
|
pauseMonitoring: "Pause Monitoring",
|
||||||
|
|
||||||
|
|
||||||
|
//IncidentTable.tsx
|
||||||
|
responseTime: "Response Time",
|
||||||
|
errorMessage: "Error Message",
|
||||||
|
details: "Details",
|
||||||
|
unmuteAlerts: "Unmute Alerts",
|
||||||
|
muteAlerts: "Mute Alerts",
|
||||||
|
|
||||||
|
//LastCheckedTime.tsx
|
||||||
|
pausedAt: "Paused at ",
|
||||||
|
lastCheckDetails: "Last Check Details",
|
||||||
|
checkedAt: "Checked at ",
|
||||||
|
monitoringPaused: "Monitoring Paused",
|
||||||
|
noAutomaticChecks: "No automatic checks",
|
||||||
|
|
||||||
|
//ServiveEditDialog.tsx
|
||||||
|
editService: "Edit Service",
|
||||||
|
editServiceDesc: "Update the details of your monitored service.",
|
||||||
|
|
||||||
|
//ServiceStatsCards.tsx
|
||||||
|
lastCheckedAt: "Last checked at {datetime}",
|
||||||
|
avg: "Avg",
|
||||||
|
lastUpChecksCount: "last {count} up checks",
|
||||||
|
basedOnlastChecksCount: "Based on last {count} checks",
|
||||||
|
totalUptime: "Total Uptime",
|
||||||
|
totalDowntime: "Total Downtime",
|
||||||
|
monitoringSettings: "Monitoring Settings",
|
||||||
|
monitoringSettingsInterval: "Checked every {interval} seconds",
|
||||||
|
monitoringSettingsType: "monitoring",
|
||||||
|
upStatusDuration: "Up for {duration}",
|
||||||
|
downStatusDuration: "Down for {duration}",
|
||||||
|
|
||||||
|
incidentHistory: "Incident History",
|
||||||
|
processing: "Processing",
|
||||||
|
back: "Back",
|
||||||
|
last20Checks: "Last 20 checks",
|
||||||
|
};
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
import { SettingsTranslations } from '../types/settings';
|
import { SettingsTranslations } from '../types/settings';
|
||||||
|
|
||||||
export const settingsTranslations: SettingsTranslations = {
|
export const settingsTranslations: SettingsTranslations = {
|
||||||
// Tabs
|
// General Settings - Tabs
|
||||||
systemSettings: "System Settings",
|
systemSettings: "System Settings",
|
||||||
mailSettings: "Mail Settings",
|
mailSettings: "Mail Settings",
|
||||||
|
|
||||||
// System Settings
|
// General Settings - System Settings
|
||||||
appName: "Application Name",
|
appName: "Application Name",
|
||||||
appURL: "Application URL",
|
appURL: "Application URL",
|
||||||
senderName: "Sender Name",
|
senderName: "Sender Name",
|
||||||
senderEmail: "Sender Email Address",
|
senderEmail: "Sender Email Address",
|
||||||
hideControls: "Hide Controls",
|
hideControls: "Hide Controls",
|
||||||
|
|
||||||
// Mail Settings
|
// General Settings - Mail Settings
|
||||||
smtpSettings: "SMTP Configuration",
|
smtpSettings: "SMTP Configuration",
|
||||||
smtpEnabled: "Enable SMTP",
|
smtpEnabled: "Enable SMTP",
|
||||||
smtpHost: "SMTP Host",
|
smtpHost: "SMTP Host",
|
||||||
@@ -24,7 +24,7 @@ export const settingsTranslations: SettingsTranslations = {
|
|||||||
enableTLS: "Enable TLS",
|
enableTLS: "Enable TLS",
|
||||||
localName: "Local Name",
|
localName: "Local Name",
|
||||||
|
|
||||||
// Test Email
|
// General Settings - Test Email
|
||||||
testEmail: "Test Email",
|
testEmail: "Test Email",
|
||||||
sendTestEmail: "Send test email",
|
sendTestEmail: "Send test email",
|
||||||
emailTemplate: "Email template",
|
emailTemplate: "Email template",
|
||||||
@@ -37,9 +37,13 @@ export const settingsTranslations: SettingsTranslations = {
|
|||||||
selectCollection: "Select collection",
|
selectCollection: "Select collection",
|
||||||
toEmailAddress: "To email address",
|
toEmailAddress: "To email address",
|
||||||
enterEmailAddress: "Enter email address",
|
enterEmailAddress: "Enter email address",
|
||||||
|
send: "Send",
|
||||||
sending: "Sending...",
|
sending: "Sending...",
|
||||||
|
testEmailSettings: "Test Email Settings",
|
||||||
// Actions and status
|
testEmailDescription: "Test whether the current Email Settings are available",
|
||||||
|
testEmailAlert: "This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.",
|
||||||
|
|
||||||
|
// General Settings - Actions and status
|
||||||
save: "Save Changes",
|
save: "Save Changes",
|
||||||
saving: "Saving...",
|
saving: "Saving...",
|
||||||
settingsUpdated: "Settings updated successfully",
|
settingsUpdated: "Settings updated successfully",
|
||||||
@@ -48,5 +52,12 @@ export const settingsTranslations: SettingsTranslations = {
|
|||||||
testConnection: "Test Connection",
|
testConnection: "Test Connection",
|
||||||
testingConnection: "Testing Connection...",
|
testingConnection: "Testing Connection...",
|
||||||
connectionSuccess: "Connection successful",
|
connectionSuccess: "Connection successful",
|
||||||
connectionFailed: "Connection failed"
|
connectionFailed: "Connection failed",
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
addUser: "Add User",
|
||||||
|
permissionNotice: "Permission Notice:",
|
||||||
|
permissionNoticeAddUser: "As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.",
|
||||||
|
loadingSettings: "Loading settings...",
|
||||||
|
loadingSettingsError: "Error loading settings",
|
||||||
};
|
};
|
||||||
@@ -112,5 +112,6 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
created: "Created",
|
created: "Created",
|
||||||
lastUpdated: "Last Updated",
|
lastUpdated: "Last Updated",
|
||||||
lastNotification: "Last Notification",
|
lastNotification: "Last Notification",
|
||||||
collectionId: "Collection ID"
|
collectionId: "Collection ID",
|
||||||
|
noCertificatesFound: "No Certificates Found",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import enTranslations from './en';
|
import enTranslations from './en';
|
||||||
import kmTranslations from './km';
|
import kmTranslations from './km';
|
||||||
import deTranslations from './de';
|
import deTranslations from './de';
|
||||||
|
import koTranslations from './ko';
|
||||||
import jaTranslations from './ja';
|
import jaTranslations from './ja';
|
||||||
import zhcnTranslations from './zhcn';
|
import zhcnTranslations from './zhcn';
|
||||||
|
|
||||||
export type Language = "en" | "km" | "de" | "ja" | "zhcn";
|
export type Language = "en" | "km" | "de" | "ko" | "ja" | "zhcn";
|
||||||
|
|
||||||
export const translations = {
|
export const translations = {
|
||||||
en: enTranslations,
|
en: enTranslations,
|
||||||
km: kmTranslations,
|
km: kmTranslations,
|
||||||
de: deTranslations,
|
de: deTranslations,
|
||||||
|
ko: koTranslations,
|
||||||
ja: jaTranslations,
|
ja: jaTranslations,
|
||||||
zhcn: zhcnTranslations,
|
zhcn: zhcnTranslations,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AboutTranslations } from '../types/about';
|
import { AboutTranslations } from '../types/about';
|
||||||
|
|
||||||
export const aboutTranslations: AboutTranslations = {
|
export const aboutTranslations: AboutTranslations = {
|
||||||
aboutCheckcle: "Checkcleについて",
|
aboutCheckCle: "Checkcleについて",
|
||||||
systemDescription: "Checkcleは、サーバーとサービスの健全性に関するリアルタイム監視、インシデント管理、運用の透明性を提供するオープンソースの監視スタックです。MIT ライセンスの下で公開されています。",
|
systemDescription: "Checkcleは、サーバーとサービスの健全性に関するリアルタイム監視、インシデント管理、運用の透明性を提供するオープンソースの監視スタックです。MIT ライセンスの下で公開されています。",
|
||||||
systemVersion: "システムバージョン",
|
systemVersion: "システムバージョン",
|
||||||
license: "ライセンス",
|
license: "ライセンス",
|
||||||
@@ -14,4 +14,5 @@ export const aboutTranslations: AboutTranslations = {
|
|||||||
quickActions: "クイックアクション",
|
quickActions: "クイックアクション",
|
||||||
quickActionsDescription: "一般的な監視操作と機能に素早くアクセスできます。開始するには、以下のアクションを選択してください。",
|
quickActionsDescription: "一般的な監視操作と機能に素早くアクセスできます。開始するには、以下のアクションを選択してください。",
|
||||||
quickTips: "クイックヒント",
|
quickTips: "クイックヒント",
|
||||||
|
releasedOn: "公開日",
|
||||||
};
|
};
|
||||||
@@ -4,11 +4,12 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
welcome: "ようこそ",
|
welcome: "ようこそ",
|
||||||
logout: "ログアウト",
|
logout: "ログアウト",
|
||||||
language: "言語",
|
language: "言語",
|
||||||
english: "English",
|
english: "Englisch",
|
||||||
khmer: "Khmer",
|
khmer: "ភាសាខ្មែរ",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
simplifiedChinese: "简体中文",
|
korean: "한국어",
|
||||||
japanese: "日本語",
|
japanese: "日本語",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
goodMorning: "おはようございます",
|
goodMorning: "おはようございます",
|
||||||
goodAfternoon: "こんにちは",
|
goodAfternoon: "こんにちは",
|
||||||
goodEvening: "こんばんは",
|
goodEvening: "こんばんは",
|
||||||
|
|||||||
@@ -51,4 +51,5 @@ export const incidentTranslations: IncidentTranslations = {
|
|||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
failedToUpdateStatus: 'ステータスの更新に失敗しました',
|
failedToUpdateStatus: 'ステータスの更新に失敗しました',
|
||||||
inProgress: '進行中',
|
inProgress: '進行中',
|
||||||
|
enterRootCause: '原因を入力してください',
|
||||||
};
|
};
|
||||||
@@ -2,10 +2,20 @@ import { ServicesTranslations } from '../types/services';
|
|||||||
|
|
||||||
export const servicesTranslations: ServicesTranslations = {
|
export const servicesTranslations: ServicesTranslations = {
|
||||||
serviceName: "サービス名",
|
serviceName: "サービス名",
|
||||||
|
serviceNameDesc: "サービスのわかりやすい名前を入力してください",
|
||||||
serviceType: "サービスタイプ",
|
serviceType: "サービスタイプ",
|
||||||
serviceStatus: "サービスステータス",
|
serviceStatus: "サービスステータス",
|
||||||
responseTime: "応答時間",
|
responseTime: "応答時間",
|
||||||
uptime: "稼働時間",
|
uptime: "稼働時間",
|
||||||
lastChecked: "最終チェック",
|
lastChecked: "最終チェック",
|
||||||
noServices: "フィルタ条件に一致するサービスがありません。",
|
noServices: "フィルタ条件に一致するサービスがありません。",
|
||||||
|
currentlyMonitoring: "現在監視中",
|
||||||
|
retry: "再試行",
|
||||||
|
overview: "概要",
|
||||||
|
newService: "新しいサービス",
|
||||||
|
rowsPerPage: "1ページあたりの行数",
|
||||||
|
search: "検索",
|
||||||
|
allTypes: "すべてのタイプ",
|
||||||
|
createNewService: "新しいサービスを作成",
|
||||||
|
createNewServiceDesc: "監視する新しいサービスを作成するには、詳細を入力してください。",
|
||||||
};
|
};
|
||||||
@@ -111,5 +111,6 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
created: "作成日",
|
created: "作成日",
|
||||||
lastUpdated: "最終更新",
|
lastUpdated: "最終更新",
|
||||||
lastNotification: "最終通知",
|
lastNotification: "最終通知",
|
||||||
collectionId: "コレクションID"
|
collectionId: "コレクションID",
|
||||||
|
noCertificatesFound: "証明書が見つかりません",
|
||||||
};
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AboutTranslations } from '../types/about';
|
import { AboutTranslations } from '../types/about';
|
||||||
|
|
||||||
export const aboutTranslations: AboutTranslations = {
|
export const aboutTranslations: AboutTranslations = {
|
||||||
aboutCheckcle: "អំពី Checkcle",
|
aboutCheckCle: "អំពី Checkcle",
|
||||||
systemDescription: "Checkcle គឺជាស្តាកការត្រួតពិនិត្យប្រភពបើកទូលាយដែលផ្តល់នូវការយល់ដឹងជាក់ស្តែងអំពីសុខភាពម៉ាស៊ីនមេ និងសេវាកម្ម, ការគ្រប់គ្រងឧបទ្ទវហេតុ, និងតម្លាភាពនៃប្រតិបត្តិការ។ ចេញផ្សាយជាអាជ្ញាបណ្ណ MIT។",
|
systemDescription: "Checkcle គឺជាស្តាកការត្រួតពិនិត្យប្រភពបើកទូលាយដែលផ្តល់នូវការយល់ដឹងជាក់ស្តែងអំពីសុខភាពម៉ាស៊ីនមេ និងសេវាកម្ម, ការគ្រប់គ្រងឧបទ្ទវហេតុ, និងតម្លាភាពនៃប្រតិបត្តិការ។ ចេញផ្សាយជាអាជ្ញាបណ្ណ MIT។",
|
||||||
systemVersion: "កំណែប្រព័ន្ធ",
|
systemVersion: "កំណែប្រព័ន្ធ",
|
||||||
license: "អាជ្ញាបណ្ណ",
|
license: "អាជ្ញាបណ្ណ",
|
||||||
@@ -15,4 +15,5 @@ export const aboutTranslations: AboutTranslations = {
|
|||||||
quickActions: "សកម្មភាពរហ័ស",
|
quickActions: "សកម្មភាពរហ័ស",
|
||||||
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
|
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
|
||||||
quickTips: "គន្លឹះរហ័ស",
|
quickTips: "គន្លឹះរហ័ស",
|
||||||
|
releasedOn: "បានចេញផ្សាយនៅថ្ងៃទី",
|
||||||
};
|
};
|
||||||
@@ -5,9 +5,11 @@ export const commonTranslations: CommonTranslations = {
|
|||||||
welcome: "សូមស្វាគមន៍",
|
welcome: "សូមស្វាគមន៍",
|
||||||
logout: "ចាកចេញ",
|
logout: "ចាកចេញ",
|
||||||
language: "ភាសា",
|
language: "ភាសា",
|
||||||
english: "អង់គ្លេស",
|
english: "Englisch",
|
||||||
khmer: "ខ្មែរ",
|
khmer: "ភាសាខ្មែរ",
|
||||||
german: "Deutsch",
|
german: "Deutsch",
|
||||||
|
korean: "한국어",
|
||||||
|
japanese: "日本語",
|
||||||
simplifiedChinese: "简体中文",
|
simplifiedChinese: "简体中文",
|
||||||
goodMorning: "អរុណសួស្តី",
|
goodMorning: "អរុណសួស្តី",
|
||||||
goodAfternoon: "ទិវាសួស្តី",
|
goodAfternoon: "ទិវាសួស្តី",
|
||||||
|
|||||||
@@ -52,4 +52,5 @@ export const incidentTranslations: IncidentTranslations = {
|
|||||||
configuration: "ការកំណត់រចនាសម្ព័ន្ធ",
|
configuration: "ការកំណត់រចនាសម្ព័ន្ធ",
|
||||||
failedToUpdateStatus: "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពស្ថានភាព",
|
failedToUpdateStatus: "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពស្ថានភាព",
|
||||||
inProgress: "កំពុងដំណើរការ",
|
inProgress: "កំពុងដំណើរការ",
|
||||||
|
enterRootCause: 'បញ្ចូលហេតុផលដើម',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,20 @@ import { ServicesTranslations } from '../types/services';
|
|||||||
|
|
||||||
export const servicesTranslations: ServicesTranslations = {
|
export const servicesTranslations: ServicesTranslations = {
|
||||||
serviceName: "ឈ្មោះសេវាកម្ម",
|
serviceName: "ឈ្មោះសេវាកម្ម",
|
||||||
|
serviceNameDesc: "បញ្ចូលឈ្មោះពិពណ៌នាសម្រាប់សេវាកម្មរបស់អ្នក",
|
||||||
serviceType: "ប្រភេទសេវាកម្ម",
|
serviceType: "ប្រភេទសេវាកម្ម",
|
||||||
serviceStatus: "ស្ថានភាពសេវាកម្ម",
|
serviceStatus: "ស្ថានភាពសេវាកម្ម",
|
||||||
responseTime: "ពេលវេលាឆ្លើយតប",
|
responseTime: "ពេលវេលាឆ្លើយតប",
|
||||||
uptime: "ពេលវេលាដំណើរការ",
|
uptime: "ពេលវេលាដំណើរការ",
|
||||||
lastChecked: "ពិនិត្យចុងក្រោយ",
|
lastChecked: "ពិនិត្យចុងក្រោយ",
|
||||||
noServices: "មិនមានសេវាកម្មដែលត្រូវនឹងលក្ខណៈវិនិច្ឆ័យរបស់អ្នក។",
|
noServices: "មិនមានសេវាកម្មដែលត្រូវនឹងលក្ខណៈវិនិច្ឆ័យរបស់អ្នក។",
|
||||||
|
currentlyMonitoring: "កំពុងតាមដានពេលនេះ",
|
||||||
|
retry: "សាកល្បងម្ដងទៀត",
|
||||||
|
overview: "ទិដ្ឋភាពទូទៅ",
|
||||||
|
newService: "សេវាកម្មថ្មី",
|
||||||
|
rowsPerPage: "ចំនួនជួរដេកក្នុងមួយទំព័រ",
|
||||||
|
search: "ស្វែងរក",
|
||||||
|
allTypes: "គ្រប់ប្រភេទ",
|
||||||
|
createNewService: "បង្កើតសេវាកម្មថ្មី",
|
||||||
|
createNewServiceDesc: "បញ្ចូលព័ត៌មានលម្អិតដើម្បីបង្កើតសេវាកម្មថ្មីដែលត្រូវតាមដាន។",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,5 +112,6 @@ export const sslTranslations: SSLTranslations = {
|
|||||||
created: "បានបង្កើត",
|
created: "បានបង្កើត",
|
||||||
lastUpdated: "បានធ្វើបច្ចុប្បន្នភាពចុងក្រោយ",
|
lastUpdated: "បានធ្វើបច្ចុប្បន្នភាពចុងក្រោយ",
|
||||||
lastNotification: "ការជូនដំណឹងចុងក្រោយ",
|
lastNotification: "ការជូនដំណឹងចុងក្រោយ",
|
||||||
collectionId: "លេខសម្គាល់ប្រមូលផ្តុំ"
|
collectionId: "លេខសម្គាល់ប្រមូលផ្តុំ",
|
||||||
|
noCertificatesFound: "រកមិនឃើញវិញ្ញាបនប័ត្រ",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { AboutTranslations } from '../types/about';
|
||||||
|
|
||||||
|
export const aboutTranslations: AboutTranslations = {
|
||||||
|
aboutCheckCle: "About Checkcle",
|
||||||
|
systemDescription: "Checkcle는 서버 및 서비스 상태, 사고 관리, 운영 투명성에 대한 실시간 인사이트를 제공하는 오픈소스 모니터링 스택입니다. MIT 라이선스로 공개됨.",
|
||||||
|
systemVersion: "시스템 버전",
|
||||||
|
license: "라이선스",
|
||||||
|
mitLicense: "MIT 라이선스",
|
||||||
|
links: "링크",
|
||||||
|
viewOnGithub: "GitHub에서 보기",
|
||||||
|
viewDocumentation: "문서 보기",
|
||||||
|
followOnX: "X에서 팔로우",
|
||||||
|
joinDiscord: "Discord 참여",
|
||||||
|
quickActions: "빠른 작업",
|
||||||
|
quickActionsDescription: "자주 사용하는 모니터링 작업과 기능에 빠르게 접근하세요. 아래에서 작업을 선택하여 시작할 수 있습니다.",
|
||||||
|
quickTips: "빠른 팁",
|
||||||
|
releasedOn: "출시일",
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { CommonTranslations } from '../types/common';
|
||||||
|
|
||||||
|
export const commonTranslations: CommonTranslations = {
|
||||||
|
welcome: "환영합니다",
|
||||||
|
logout: "로그아웃",
|
||||||
|
language: "언어",
|
||||||
|
english: "Englisch",
|
||||||
|
khmer: "ភាសាខ្មែរ",
|
||||||
|
german: "Deutsch",
|
||||||
|
korean: "한국어",
|
||||||
|
japanese: "日本語",
|
||||||
|
simplifiedChinese: "简体中文",
|
||||||
|
goodMorning: "좋은 아침입니다",
|
||||||
|
goodAfternoon: "좋은 오후입니다",
|
||||||
|
goodEvening: "좋은 저녁입니다",
|
||||||
|
profile: "프로필",
|
||||||
|
settings: "설정",
|
||||||
|
documentation: "문서",
|
||||||
|
notifications: "알림",
|
||||||
|
close: "닫기",
|
||||||
|
cancel: "취소",
|
||||||
|
view: "보기",
|
||||||
|
edit: "편집",
|
||||||
|
delete: "삭제",
|
||||||
|
status: "상태",
|
||||||
|
time: "시간",
|
||||||
|
title: "제목",
|
||||||
|
description: "설명",
|
||||||
|
success: "성공",
|
||||||
|
error: "오류",
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
|
||||||
|
import { IncidentTranslations } from '../types/incident';
|
||||||
|
|
||||||
|
export const incidentTranslations: IncidentTranslations = {
|
||||||
|
incidentManagement: '인시던트 관리',
|
||||||
|
incidentsManagementDesc: '서비스 인시던트 및 해결 과정을 추적하고 관리합니다',
|
||||||
|
unresolvedIncidents: '미해결',
|
||||||
|
resolvedIncidents: '해결됨',
|
||||||
|
activeIncidents: '활성 인시던트',
|
||||||
|
criticalIssues: '중대한 문제',
|
||||||
|
avgResolutionTime: '평균 해결 시간',
|
||||||
|
noIncidents: '활성 인시던트 없음',
|
||||||
|
createIncident: '인시던트 생성',
|
||||||
|
investigating: '조사 중',
|
||||||
|
identified: '확인됨',
|
||||||
|
monitoring: '모니터링 중',
|
||||||
|
resolved: '해결됨',
|
||||||
|
scheduleIncidentManagement: '일정 및 인시던트 관리',
|
||||||
|
incidentName: '인시던트 이름',
|
||||||
|
incidentStatus: '인시던트 상태',
|
||||||
|
highPriority: '높은 우선순위',
|
||||||
|
configurationSettings: '구성 설정',
|
||||||
|
incidentCreatedSuccess: '인시던트가 성공적으로 생성되었습니다',
|
||||||
|
basicInfo: '기본 정보',
|
||||||
|
serviceId: '서비스 ID',
|
||||||
|
assignedTo: '담당자',
|
||||||
|
unassigned: '미할당',
|
||||||
|
timeline: '타임라인',
|
||||||
|
incidentTime: '인시던트 발생 시간',
|
||||||
|
resolutionTime: '해결 시간',
|
||||||
|
systems: '시스템',
|
||||||
|
noSystems: '영향을 받은 시스템 없음',
|
||||||
|
impactAnalysis: '영향 분석',
|
||||||
|
rootCause: '근본 원인',
|
||||||
|
resolutionSteps: '해결 단계',
|
||||||
|
lessonsLearned: '교훈',
|
||||||
|
resolutionDetails: '해결 상세',
|
||||||
|
assignment: '할당',
|
||||||
|
download: '다운로드',
|
||||||
|
downloadPdf: 'PDF 다운로드',
|
||||||
|
print: '인쇄',
|
||||||
|
confidentialNote: '이 문서는 기밀이며 내부 용도로만 사용됩니다.',
|
||||||
|
generatedOn: '생성일',
|
||||||
|
enterResolutionSteps: '인시던트 해결 단계 입력',
|
||||||
|
enterLessonsLearned: '인시던트 교훈 입력',
|
||||||
|
editIncident: '인시던트 편집',
|
||||||
|
editIncidentDesc: '인시던트 세부 정보 업데이트',
|
||||||
|
updating: '업데이트 중...',
|
||||||
|
update: '업데이트',
|
||||||
|
create: '생성',
|
||||||
|
creating: '생성 중...',
|
||||||
|
configuration: '구성',
|
||||||
|
failedToUpdateStatus: '상태 업데이트 실패',
|
||||||
|
inProgress: '진행 중',
|
||||||
|
enterRootCause: '근본 원인 입력',
|
||||||
|
enterIncidentTitle: '인시던트 제목 입력',
|
||||||
|
enterIncidentDescription: '인시던트 설명 입력',
|
||||||
|
enterServiceId: '서비스 ID를 콤마로 구분하여 입력',
|
||||||
|
selectAssignedUser: '담당자 선택',
|
||||||
|
noAssignedUser: '현재 인시던트에 할당된 사용자가 없습니다',
|
||||||
|
affectedSystems: '영향을 받은 시스템',
|
||||||
|
enterAffectedSystems: '영향을 받은 시스템 입력',
|
||||||
|
separateSystemsWithComma: '영향 시스템을 콤마로 구분하여 입력',
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
import { Translations } from '../types';
|
||||||
|
import { commonTranslations } from './common';
|
||||||
|
import { menuTranslations } from './menu';
|
||||||
|
import { loginTranslations } from './login';
|
||||||
|
import { aboutTranslations } from './about';
|
||||||
|
import { servicesTranslations } from './services';
|
||||||
|
import { maintenanceTranslations } from './maintenance';
|
||||||
|
import { incidentTranslations } from './incident';
|
||||||
|
import { sslTranslations } from './ssl';
|
||||||
|
import { settingsTranslations } from './settings';
|
||||||
|
|
||||||
|
const enTranslations: Translations = {
|
||||||
|
common: commonTranslations,
|
||||||
|
menu: menuTranslations,
|
||||||
|
login: loginTranslations,
|
||||||
|
about: aboutTranslations,
|
||||||
|
services: servicesTranslations,
|
||||||
|
maintenance: maintenanceTranslations,
|
||||||
|
incident: incidentTranslations,
|
||||||
|
ssl: sslTranslations,
|
||||||
|
settings: settingsTranslations
|
||||||
|
};
|
||||||
|
|
||||||
|
export default enTranslations;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import { LoginTranslations } from '../types/login';
|
||||||
|
|
||||||
|
export const loginTranslations: LoginTranslations = {
|
||||||
|
signInToYourAccount: "계정 로그인",
|
||||||
|
dontHaveAccount: "계정이 없으신가요?",
|
||||||
|
createOne: "계정 생성",
|
||||||
|
signInWithGoogle: "Google로 로그인",
|
||||||
|
orContinueWith: "또는",
|
||||||
|
email: "이메일",
|
||||||
|
password: "비밀번호",
|
||||||
|
forgot: "잊으셨나요?",
|
||||||
|
signIn: "로그인",
|
||||||
|
signingIn: "로그인 중...",
|
||||||
|
loginSuccessful: "로그인 성공",
|
||||||
|
loginSuccessMessage: "성공적으로 로그인되었습니다.",
|
||||||
|
loginFailed: "로그인 실패",
|
||||||
|
authenticationFailed: "인증 실패",
|
||||||
|
bySigningIn: "로그인하면 다음에 동의하게 됩니다:",
|
||||||
|
termsAndConditions: "이용약관",
|
||||||
|
and: "및",
|
||||||
|
privacyPolicy: "개인정보 처리방침",
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
import { MaintenanceTranslations } from '../types/maintenance';
|
||||||
|
|
||||||
|
export const maintenanceTranslations: MaintenanceTranslations = {
|
||||||
|
scheduledMaintenance: '예정된 유지보수',
|
||||||
|
scheduledMaintenanceDesc: '시스템 및 서비스에 계획된 유지보수 일정을 보고 관리합니다',
|
||||||
|
upcomingMaintenance: '예정',
|
||||||
|
ongoingMaintenance: '진행 중',
|
||||||
|
completedMaintenance: '완료됨',
|
||||||
|
createMaintenanceWindow: '유지보수 생성',
|
||||||
|
totalScheduledHours: '총 예정 시간',
|
||||||
|
maintenanceName: '유지보수 이름',
|
||||||
|
maintenanceStatus: '상태',
|
||||||
|
scheduledStart: '예정 시작',
|
||||||
|
scheduledEnd: '예정 종료',
|
||||||
|
affectedServices: '영향받는 서비스',
|
||||||
|
impact: '영향',
|
||||||
|
minor: '경미',
|
||||||
|
major: '중대',
|
||||||
|
critical: '심각',
|
||||||
|
none: '없음',
|
||||||
|
actions: '작업',
|
||||||
|
scheduled: '예정됨',
|
||||||
|
inprogress: '진행 중',
|
||||||
|
completed: '완료됨',
|
||||||
|
cancelled: '취소됨',
|
||||||
|
markAsInProgress: '진행 중으로 표시',
|
||||||
|
markAsCompleted: '완료로 표시',
|
||||||
|
markAsCancelled: '취소로 표시',
|
||||||
|
confirmDelete: '삭제 확인',
|
||||||
|
deleteMaintenanceConfirmation: '이 유지보수 일정을 삭제하시겠습니까?',
|
||||||
|
thisActionCannotBeUndone: '이 작업은 되돌릴 수 없습니다.',
|
||||||
|
maintenanceDeleted: '유지보수 삭제됨',
|
||||||
|
maintenanceDeletedDesc: '유지보수 일정이 성공적으로 삭제되었습니다.',
|
||||||
|
errorDeletingMaintenance: '유지보수 삭제 중 오류가 발생했습니다.',
|
||||||
|
statusUpdated: '상태 업데이트됨',
|
||||||
|
maintenanceStatusUpdated: '유지보수 상태가 성공적으로 업데이트되었습니다.',
|
||||||
|
errorUpdatingMaintenanceStatus: '유지보수 상태 업데이트 중 오류가 발생했습니다.',
|
||||||
|
createMaintenance: '유지보수 생성',
|
||||||
|
createMaintenanceDesc: '서비스를 위한 새 유지보수 일정을 예약합니다',
|
||||||
|
enterTitle: '유지보수 제목 입력',
|
||||||
|
enterDescription: '유지보수 상세 설명 입력',
|
||||||
|
startTime: '시작 시간',
|
||||||
|
endTime: '종료 시간',
|
||||||
|
selectDate: '날짜 선택',
|
||||||
|
enterAffectedServices: '영향받는 서비스 입력',
|
||||||
|
separateServicesWithComma: '여러 서비스를 콤마로 구분하여 입력',
|
||||||
|
priority: '우선순위',
|
||||||
|
selectPriority: '우선순위 선택',
|
||||||
|
selectStatus: '상태 선택',
|
||||||
|
selectImpact: '영향 선택',
|
||||||
|
notifySubscribers: '구독자에게 알림',
|
||||||
|
notifySubscribersDesc: '유지보수 시작 시 모든 구독자에게 알림 전송',
|
||||||
|
maintenanceCreated: '유지보수 생성됨',
|
||||||
|
maintenanceCreatedDesc: '유지보수 일정이 성공적으로 예약되었습니다.',
|
||||||
|
errorCreatingMaintenance: '유지보수 생성 중 오류 발생',
|
||||||
|
errorFetchingMaintenanceData: '유지보수 데이터 가져오기 중 오류 발생',
|
||||||
|
low: '낮음',
|
||||||
|
medium: '중간',
|
||||||
|
high: '높음',
|
||||||
|
created: '생성됨',
|
||||||
|
lastUpdated: '마지막 업데이트',
|
||||||
|
subscribersWillBeNotified: '유지보수가 시작되면 구독자에게 알림이 전송됩니다',
|
||||||
|
noNotifications: '알림이 전송되지 않습니다',
|
||||||
|
noScheduledMaintenance: '예정된 유지보수 없음',
|
||||||
|
noMaintenanceWindows: '이 기간에 유지보수 일정이 없습니다. "유지보수 생성" 버튼을 클릭하여 새 일정을 생성하세요.',
|
||||||
|
maintenanceCreatedSuccess: '유지보수 일정이 성공적으로 생성되었습니다',
|
||||||
|
editMaintenanceWindow: '유지보수 편집',
|
||||||
|
editMaintenanceDesc: '유지보수 세부정보 편집',
|
||||||
|
priorityDescription: '유지보수 우선순위 선택',
|
||||||
|
statusDescription: '유지보수 상태 선택',
|
||||||
|
impactLevel: '영향 수준',
|
||||||
|
impactLevelDescription: '영향 수준 선택',
|
||||||
|
selectAssignedUsers: '담당 사용자 선택',
|
||||||
|
assignedPersonnelDescription: '현재 할당된 사용자',
|
||||||
|
mutedNotifications: '알림 비활성화됨',
|
||||||
|
notificationsAreMuted: '현재 유지보수 알림이 비활성화되어 있습니다',
|
||||||
|
selectNotificationChannel: '알림 채널 추가',
|
||||||
|
enableNotificationsFirst: '채널을 선택하려면 먼저 알림을 활성화하세요',
|
||||||
|
updateMaintenance: '유지보수 업데이트',
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
import { MenuTranslations } from '../types/menu';
|
||||||
|
|
||||||
|
export const menuTranslations: MenuTranslations = {
|
||||||
|
uptimeMonitoring: "가동 시간 모니터링",
|
||||||
|
instanceMonitoring: "인스턴스 모니터링",
|
||||||
|
sslDomain: "SSL & 도메인",
|
||||||
|
scheduleIncident: "일정 및 인시던트",
|
||||||
|
operationalPage: "운영 페이지",
|
||||||
|
reports: "리포트",
|
||||||
|
regionalMonitoring: "지역 모니터링",
|
||||||
|
settingPanel: "설정 패널",
|
||||||
|
generalSettings: "일반 설정",
|
||||||
|
userManagement: "사용자 관리",
|
||||||
|
notificationSettings: "알림 설정",
|
||||||
|
alertsTemplates: "알림 템플릿",
|
||||||
|
rolesManagement: "권한 관리",
|
||||||
|
dataRetention: "데이터 보존",
|
||||||
|
backupSettings: "백업 설정",
|
||||||
|
aboutSystem: "시스템 정보",
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
import { ServicesTranslations } from '../types/services';
|
||||||
|
|
||||||
|
export const servicesTranslations: ServicesTranslations = {
|
||||||
|
serviceStatus: "서비스 상태",
|
||||||
|
responseTime: "응답 시간",
|
||||||
|
uptime: "가동 시간",
|
||||||
|
lastChecked: "마지막 확인",
|
||||||
|
noServices: "필터 조건에 맞는 서비스가 없습니다.",
|
||||||
|
currentlyMonitoring: "현재 모니터링 중",
|
||||||
|
retry: "재시도",
|
||||||
|
overview: "개요",
|
||||||
|
newService: "새 서비스",
|
||||||
|
rowsPerPage: "페이지당 행",
|
||||||
|
search: "검색",
|
||||||
|
allTypes: "모든 유형",
|
||||||
|
createNewService: "새 서비스 생성",
|
||||||
|
createNewServiceDesc: "모니터링할 새 서비스의 세부 정보를 입력하세요.",
|
||||||
|
|
||||||
|
serviceName: "서비스 이름",
|
||||||
|
serviceNameDesc: "서비스에 대한 설명적인 이름을 입력하세요",
|
||||||
|
|
||||||
|
checkInterval: "체크 간격",
|
||||||
|
seconds: "초",
|
||||||
|
minute: "분",
|
||||||
|
minutes: "분",
|
||||||
|
hour: "시간",
|
||||||
|
hours: "시간",
|
||||||
|
custom: "사용자 정의",
|
||||||
|
checkIntervalPlaceholder: "초 단위로 간격 입력",
|
||||||
|
backToPresets: "기본값으로 돌아가기",
|
||||||
|
checkIntervalDesc: "서비스 상태를 확인하는 간격",
|
||||||
|
checkIntervalDescCustom: "사용자 정의 간격 입력 (최소 10초)",
|
||||||
|
retryAttempts: "재시도 횟수",
|
||||||
|
attempt: "회",
|
||||||
|
attempts: "회",
|
||||||
|
retryAttemptsDesc: "서비스 다운으로 표시되기 전 재시도 횟수",
|
||||||
|
|
||||||
|
updateService: "서비스 업데이트",
|
||||||
|
createService: "서비스 생성",
|
||||||
|
|
||||||
|
enableNotifications: "알림 활성화",
|
||||||
|
enableNotificationsDesc: "서비스 알림을 활성화하거나 비활성화합니다",
|
||||||
|
notificationChannels: "알림 채널",
|
||||||
|
notificationChannelsEnabledDesc: "서비스 알림 채널 선택",
|
||||||
|
notificationChannelsDesc: "채널을 선택하려면 먼저 알림을 활성화하세요",
|
||||||
|
notificationChannelsPlaceholder: "알림 채널 추가",
|
||||||
|
alertTemplate: "알림 템플릿",
|
||||||
|
alertTemplateLoading: "템플릿 로딩 중...",
|
||||||
|
alertTemplatePlaceholder: "알림 템플릿 선택",
|
||||||
|
alertTemplateEnabledDesc: "알림 메시지용 템플릿 선택",
|
||||||
|
alertTemplateDesc: "템플릿 선택 전 알림을 활성화하세요",
|
||||||
|
|
||||||
|
serviceType: "서비스 유형",
|
||||||
|
serviceTypeHTTPDesc: "HTTP/HTTPS 프로토콜로 웹사이트 및 REST API 모니터링",
|
||||||
|
serviceTypePINGDesc: "PING 프로토콜로 호스트 가용성 모니터링",
|
||||||
|
serviceTypeTCPDesc: "TCP 프로토콜로 TCP 포트 연결 상태 모니터링",
|
||||||
|
serviceTypeDNSDesc: "DNS 해석 모니터링",
|
||||||
|
|
||||||
|
regionalMonitoring: "지역 모니터링",
|
||||||
|
regionalMonitoringDesc: "이 서비스를 지역 모니터링 에이전트에 할당하여 분산 모니터링",
|
||||||
|
regionalAgents: "지역 에이전트",
|
||||||
|
regionalAgentsLoading: "에이전트 로딩 중...",
|
||||||
|
regionalAgentsAvailablePlaceholder: "추가 지역 에이전트 선택...",
|
||||||
|
regionalAgentsAllSelected: "모든 사용 가능한 에이전트 선택됨",
|
||||||
|
regionalAgentsNoAvailable: "사용 가능한 지역 에이전트 없음",
|
||||||
|
regionalAgentsNoOnlineAvailable: "온라인 지역 에이전트 없음",
|
||||||
|
regionalAgentsNotFoundMessage: "온라인 지역 에이전트 없음. 기본 모니터링 사용",
|
||||||
|
regionalAgentsNotSelectedMessage: "지역 에이전트 선택 안됨. 기본 모니터링 사용",
|
||||||
|
|
||||||
|
targetDefault: "대상 URL/호스트",
|
||||||
|
targetDNS: "도메인 이름",
|
||||||
|
targetHTTPDesc: "프로토콜 포함 전체 URL 입력 (http:// 또는 https://)",
|
||||||
|
targetPINGDesc: "호스트 이름 또는 IP 주소 입력",
|
||||||
|
targetTCPDesc: "TCP 연결 테스트용 호스트 이름 또는 IP 입력",
|
||||||
|
targetTCPPortDesc: "TCP 연결 테스트용 포트 번호 입력",
|
||||||
|
targetDNSDesc: "DNS 레코드 모니터링용 도메인 이름 입력 (A, AAAA, MX 등)",
|
||||||
|
targetDefaultDesc: "모니터링 대상 URL 또는 호스트 이름 입력",
|
||||||
|
targetDefaultPlaceholder: "URL 또는 호스트 이름 입력",
|
||||||
|
|
||||||
|
serviceNameRequired: "서비스 이름은 필수입니다",
|
||||||
|
urlDomainHostRequired: "URL/도메인/호스트는 필수입니다",
|
||||||
|
enterValidUrlHostnameDomain: "유효한 URL, 호스트 이름 또는 도메인을 입력하세요",
|
||||||
|
|
||||||
|
upServices: "가동 서비스",
|
||||||
|
downServices: "다운 서비스",
|
||||||
|
pausedServices: "일시정지 서비스",
|
||||||
|
warningServices: "경고 서비스",
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
import { SettingsTranslations } from '../types/settings';
|
||||||
|
|
||||||
|
export const settingsTranslations: SettingsTranslations = {
|
||||||
|
// General Settings - Tabs
|
||||||
|
systemSettings: "시스템 설정",
|
||||||
|
mailSettings: "메일 설정",
|
||||||
|
|
||||||
|
// General Settings - System Settings
|
||||||
|
appName: "앱 이름",
|
||||||
|
appURL: "앱 URL",
|
||||||
|
senderName: "발신자 이름",
|
||||||
|
senderEmail: "발신자 이메일 주소",
|
||||||
|
hideControls: "컨트롤 숨기기",
|
||||||
|
|
||||||
|
// General Settings - Mail Settings
|
||||||
|
smtpSettings: "SMTP 구성",
|
||||||
|
smtpEnabled: "SMTP 활성화",
|
||||||
|
smtpHost: "SMTP 호스트",
|
||||||
|
smtpPort: "SMTP 포트",
|
||||||
|
smtpUsername: "SMTP 사용자 이름",
|
||||||
|
smtpPassword: "SMTP 비밀번호",
|
||||||
|
smtpAuthMethod: "인증 방법",
|
||||||
|
enableTLS: "TLS 활성화",
|
||||||
|
localName: "로컬 이름",
|
||||||
|
|
||||||
|
// General Settings - Test Email
|
||||||
|
testEmail: "테스트 이메일",
|
||||||
|
sendTestEmail: "테스트 이메일 전송",
|
||||||
|
emailTemplate: "이메일 템플릿",
|
||||||
|
verification: "검증",
|
||||||
|
passwordReset: "비밀번호 재설정",
|
||||||
|
confirmEmailChange: "이메일 변경 확인",
|
||||||
|
otp: "OTP",
|
||||||
|
loginAlert: "로그인 알림",
|
||||||
|
authCollection: "인증 컬렉션",
|
||||||
|
selectCollection: "컬렉션 선택",
|
||||||
|
toEmailAddress: "받는 이메일",
|
||||||
|
enterEmailAddress: "이메일 주소 입력",
|
||||||
|
sending: "전송 중...",
|
||||||
|
|
||||||
|
// General Settings - Actions and status
|
||||||
|
save: "변경 사항 저장",
|
||||||
|
saving: "저장 중...",
|
||||||
|
settingsUpdated: "설정이 성공적으로 업데이트되었습니다",
|
||||||
|
errorSavingSettings: "설정 저장 중 오류 발생",
|
||||||
|
errorFetchingSettings: "설정 불러오기 중 오류 발생",
|
||||||
|
testConnection: "연결 테스트",
|
||||||
|
testingConnection: "연결 테스트 중...",
|
||||||
|
connectionSuccess: "연결 성공",
|
||||||
|
connectionFailed: "연결 실패",
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
addUser: "사용자 추가",
|
||||||
|
permissionNotice: "권한 안내:",
|
||||||
|
permissionNoticeAddUser: "관리자 계정은 시스템 및 메일 설정을 확인하거나 변경할 수 없습니다. 이 설정은 슈퍼 관리자만 접근 및 수정할 수 있습니다. 시스템 구성 또는 메일 설정 변경이 필요하면 슈퍼 관리자에게 문의하세요.",
|
||||||
|
loadingSettings: "설정 로딩 중...",
|
||||||
|
loadingSettingsError: "설정 로딩 중 오류 발생",
|
||||||
|
};
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
import { SSLTranslations } from '../types/ssl';
|
||||||
|
|
||||||
|
export const sslTranslations: SSLTranslations = {
|
||||||
|
// Page and section titles
|
||||||
|
sslDomainManagement: "SSL & 도메인 관리",
|
||||||
|
monitorSSLCertificates: "도메인의 SSL 인증서를 모니터링하고 관리",
|
||||||
|
addSSLCertificate: "SSL 인증서 추가",
|
||||||
|
editSSLCertificate: "SSL 인증서 편집",
|
||||||
|
deleteSSLCertificate: "SSL 인증서 삭제",
|
||||||
|
sslCertificateDetails: "SSL 인증서 상세",
|
||||||
|
detailedInfo: "상세 정보",
|
||||||
|
viewDetailedInformation: "상세 정보 보기",
|
||||||
|
|
||||||
|
// Status related
|
||||||
|
valid: "유효",
|
||||||
|
expiringSoon: "곧 만료",
|
||||||
|
expired: "만료됨",
|
||||||
|
pending: "대기 중",
|
||||||
|
|
||||||
|
// Statistics and cards
|
||||||
|
validCertificates: "유효 인증서",
|
||||||
|
expiringSoonCertificates: "곧 만료 인증서",
|
||||||
|
expiredCertificates: "만료 인증서",
|
||||||
|
|
||||||
|
// Form fields
|
||||||
|
domain: "도메인",
|
||||||
|
domainName: "도메인 이름",
|
||||||
|
domainCannotChange: "생성 후 도메인은 변경할 수 없습니다",
|
||||||
|
warningThreshold: "경고 임계값",
|
||||||
|
warningThresholdDays: "경고 임계값(일)",
|
||||||
|
expiryThreshold: "만료 임계값",
|
||||||
|
expiryThresholdDays: "만료 임계값(일)",
|
||||||
|
notificationChannel: "알림 채널",
|
||||||
|
chooseChannel: "알림 채널 선택",
|
||||||
|
whereToSend: "알림 전송 위치",
|
||||||
|
daysBeforeExpiration: "만료 전 경고 받을 일수",
|
||||||
|
daysBeforeCritical: "만료 임박 시 알림 받을 일수",
|
||||||
|
getNotifiedExpiration: "인증서 만료 시 알림 받기",
|
||||||
|
getNotifiedCritical: "인증서 임박 시 알림 받기",
|
||||||
|
|
||||||
|
// Table headers and fields
|
||||||
|
issuer: "발급자",
|
||||||
|
expirationDate: "만료일",
|
||||||
|
daysLeft: "남은 일수",
|
||||||
|
status: "상태",
|
||||||
|
lastNotified: "마지막 알림",
|
||||||
|
actions: "작업",
|
||||||
|
validFrom: "유효 시작",
|
||||||
|
validUntil: "유효 종료",
|
||||||
|
validityDays: "유효 일수",
|
||||||
|
validityPeriod: "유효 기간",
|
||||||
|
organization: "조직",
|
||||||
|
commonName: "공통 이름",
|
||||||
|
serialNumber: "일련번호",
|
||||||
|
algorithm: "알고리즘",
|
||||||
|
subjectAltNames: "대체 이름",
|
||||||
|
subjectAlternativeNames: "대체 이름",
|
||||||
|
resolvedIP: "해결된 IP",
|
||||||
|
issuedTo: "발급 대상",
|
||||||
|
days: "일",
|
||||||
|
|
||||||
|
// Buttons and actions
|
||||||
|
addDomain: "도메인 추가",
|
||||||
|
refreshAll: "모두 새로고침",
|
||||||
|
cancel: "취소",
|
||||||
|
addCertificate: "인증서 추가",
|
||||||
|
check: "확인",
|
||||||
|
view: "보기",
|
||||||
|
edit: "편집",
|
||||||
|
delete: "삭제",
|
||||||
|
close: "닫기",
|
||||||
|
saveChanges: "변경 사항 저장",
|
||||||
|
updating: "업데이트 중",
|
||||||
|
|
||||||
|
// Sections in detail view
|
||||||
|
basicInformation: "기본 정보",
|
||||||
|
validity: "유효성",
|
||||||
|
issuerInfo: "발급자 정보",
|
||||||
|
technicalDetails: "기술 세부정보",
|
||||||
|
technicalInformation: "기술 정보",
|
||||||
|
monitoringConfig: "모니터링 구성",
|
||||||
|
monitoringConfiguration: "모니터링 구성",
|
||||||
|
recordInfo: "레코드 정보",
|
||||||
|
certificateDetails: "인증서 상세",
|
||||||
|
|
||||||
|
// Notifications and messages
|
||||||
|
sslCertificateAdded: "SSL 인증서가 성공적으로 추가되었습니다",
|
||||||
|
sslCertificateUpdated: "SSL 인증서가 성공적으로 업데이트되었습니다",
|
||||||
|
sslCertificateDeleted: "SSL 인증서가 성공적으로 삭제되었습니다",
|
||||||
|
sslCertificateRefreshed: "{domain}의 SSL 인증서가 성공적으로 갱신되었습니다",
|
||||||
|
allCertificatesRefreshed: "총 {count}개의 인증서가 성공적으로 갱신되었습니다",
|
||||||
|
someCertificatesFailed: "{success}개의 인증서 갱신 성공, {failed}개 실패",
|
||||||
|
failedToAddCertificate: "SSL 인증서 추가 실패",
|
||||||
|
failedToLoadCertificates: "SSL 인증서 불러오기 실패",
|
||||||
|
failedToUpdateCertificate: "SSL 인증서 업데이트 실패",
|
||||||
|
failedToDeleteCertificate: "SSL 인증서 삭제 실패",
|
||||||
|
failedToCheckCertificate: "SSL 인증서 확인 실패",
|
||||||
|
noCertificatesToRefresh: "갱신할 인증서 없음",
|
||||||
|
startingRefreshAll: "{count}개의 인증서 갱신 시작",
|
||||||
|
checkingSSLCertificate: "SSL 인증서 확인 중...",
|
||||||
|
deleteConfirmation: "인증서를 삭제하시겠습니까?",
|
||||||
|
deleteWarning: "이 작업은 되돌릴 수 없으며 인증서를 영구적으로 삭제합니다.",
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
unknown: "알 수 없음",
|
||||||
|
never: "없음",
|
||||||
|
none: "없음",
|
||||||
|
loadingChannels: "채널 로딩 중...",
|
||||||
|
noChannelsFound: "알림 채널 없음",
|
||||||
|
noSSLCertificates: "SSL 인증서 없음",
|
||||||
|
created: "생성됨",
|
||||||
|
lastUpdated: "마지막 업데이트",
|
||||||
|
lastNotification: "마지막 알림",
|
||||||
|
collectionId: "컬렉션 ID",
|
||||||
|
noCertificatesFound: "인증서 없음",
|
||||||
|
};
|
||||||
@@ -13,4 +13,20 @@ export interface AboutTranslations {
|
|||||||
quickActions: string;
|
quickActions: string;
|
||||||
quickActionsDescription: string;
|
quickActionsDescription: string;
|
||||||
quickTips: string;
|
quickTips: string;
|
||||||
|
releasedOn: string;
|
||||||
|
updateSchema: string;
|
||||||
|
updateSchemaDesc: string;
|
||||||
|
mergeFieldsLabel: string;
|
||||||
|
importing: string;
|
||||||
|
clickToUpdateSchema: string;
|
||||||
|
importSuccessful: string;
|
||||||
|
importFailed: string;
|
||||||
|
instructions: string;
|
||||||
|
mergeFields: string;
|
||||||
|
instructionsMergeFields: string;
|
||||||
|
instructionsCollections: string;
|
||||||
|
instructionsImportAuth: string;
|
||||||
|
collectionsUpdatedCount: string;
|
||||||
|
collectionsCreatedCount: string;
|
||||||
|
collectionsSkippedCount: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface CommonTranslations {
|
|||||||
english: string;
|
english: string;
|
||||||
khmer: string;
|
khmer: string;
|
||||||
german: string;
|
german: string;
|
||||||
|
korean: string;
|
||||||
japanese: string;
|
japanese: string;
|
||||||
simplifiedChinese: string;
|
simplifiedChinese: string;
|
||||||
goodMorning: string;
|
goodMorning: string;
|
||||||
|
|||||||
@@ -50,4 +50,14 @@ export interface IncidentTranslations {
|
|||||||
configuration: string;
|
configuration: string;
|
||||||
failedToUpdateStatus: string;
|
failedToUpdateStatus: string;
|
||||||
inProgress: string;
|
inProgress: string;
|
||||||
|
enterRootCause: string;
|
||||||
|
enterIncidentTitle: string;
|
||||||
|
enterIncidentDescription: string;
|
||||||
|
enterServiceId: string;
|
||||||
|
selectAssignedUser: string;
|
||||||
|
noAssignedUser: string;
|
||||||
|
affectedSystems: string;
|
||||||
|
enterAffectedSystems: string;
|
||||||
|
separateSystemsWithComma: string;
|
||||||
|
createIncidentDesc: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,4 +63,25 @@ export interface MaintenanceTranslations {
|
|||||||
noScheduledMaintenance: string;
|
noScheduledMaintenance: string;
|
||||||
noMaintenanceWindows: string;
|
noMaintenanceWindows: string;
|
||||||
maintenanceCreatedSuccess: string;
|
maintenanceCreatedSuccess: string;
|
||||||
|
editMaintenanceWindow: string;
|
||||||
|
editMaintenanceDesc: string;
|
||||||
|
priorityDescription: string;
|
||||||
|
statusDescription: string;
|
||||||
|
impactLevel: string;
|
||||||
|
impactLevelDescription: string;
|
||||||
|
selectAssignedUsers: string;
|
||||||
|
assignedPersonnelDescription: string;
|
||||||
|
mutedNotifications: string;
|
||||||
|
notificationsAreMuted: string;
|
||||||
|
selectNotificationChannel: string;
|
||||||
|
enableNotificationsFirst: string;
|
||||||
|
updateMaintenance: string;
|
||||||
|
loginToViewProfile: string;
|
||||||
|
goToLogin: string;
|
||||||
|
loadingUserData: string;
|
||||||
|
retry: string;
|
||||||
|
loadingServerData: string;
|
||||||
|
retrievingYourInformation: string;
|
||||||
|
servicesPagination: string;
|
||||||
|
servicesPaginationNoService: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,137 @@
|
|||||||
|
|
||||||
export interface ServicesTranslations {
|
export interface ServicesTranslations {
|
||||||
serviceName: string;
|
|
||||||
serviceType: string;
|
|
||||||
serviceStatus: string;
|
serviceStatus: string;
|
||||||
responseTime: string;
|
|
||||||
uptime: string;
|
uptime: string;
|
||||||
lastChecked: string;
|
lastChecked: string;
|
||||||
noServices: string;
|
noServices: string;
|
||||||
|
currentlyMonitoring: string;
|
||||||
|
retry: string;
|
||||||
|
overview: string;
|
||||||
|
newService: string;
|
||||||
|
rowsPerPage: string;
|
||||||
|
search: string;
|
||||||
|
allTypes: string;
|
||||||
|
createNewService: string;
|
||||||
|
createNewServiceDesc: string;
|
||||||
|
|
||||||
|
// ServiceBasicFields.tsx
|
||||||
|
serviceName: string;
|
||||||
|
serviceNameDesc: string;
|
||||||
|
|
||||||
|
// ServiceConfigFields.tsx
|
||||||
|
checkInterval: string;
|
||||||
|
seconds: string;
|
||||||
|
minute: string;
|
||||||
|
minutes: string;
|
||||||
|
hour: string;
|
||||||
|
hours: string;
|
||||||
|
custom: string;
|
||||||
|
checkIntervalPlaceholder: string;
|
||||||
|
backToPresets: string;
|
||||||
|
checkIntervalDesc: string;
|
||||||
|
checkIntervalDescCustom: string;
|
||||||
|
retryAttempts: string;
|
||||||
|
attempt: string;
|
||||||
|
attempts: string;
|
||||||
|
retryAttemptsDesc: string;
|
||||||
|
|
||||||
|
// ServiceForm.tsx
|
||||||
|
updateService: string;
|
||||||
|
createService: string;
|
||||||
|
|
||||||
|
// ServiceNotificationFields.tsx
|
||||||
|
enableNotifications: string;
|
||||||
|
enableNotificationsDesc: string;
|
||||||
|
notificationChannels: string;
|
||||||
|
notificationChannelsEnabledDesc: string;
|
||||||
|
notificationChannelsDesc: string;
|
||||||
|
notificationChannelsPlaceholder: string;
|
||||||
|
alertTemplate: string;
|
||||||
|
alertTemplateLoading: string;
|
||||||
|
alertTemplatePlaceholder: string;
|
||||||
|
alertTemplateEnabledDesc: string;
|
||||||
|
alertTemplateDesc: string;
|
||||||
|
|
||||||
|
// ServiceTypeField.tsx
|
||||||
|
serviceType: string;
|
||||||
|
serviceTypeHTTPDesc: string;
|
||||||
|
serviceTypePINGDesc: string;
|
||||||
|
serviceTypeTCPDesc: string;
|
||||||
|
serviceTypeDNSDesc: string;
|
||||||
|
|
||||||
|
// ServiceRegionalFields.tsx
|
||||||
|
regionalMonitoring: string;
|
||||||
|
regionalMonitoringDesc: string;
|
||||||
|
regionalAgents: string;
|
||||||
|
regionalAgentsLoading: string;
|
||||||
|
regionalAgentsAvailablePlaceholder: string;
|
||||||
|
regionalAgentsAllSelected: string;
|
||||||
|
regionalAgentsNoAvailable: string;
|
||||||
|
regionalAgentsNoOnlineAvailable: string;
|
||||||
|
regionalAgentsNotFoundMessage: string;
|
||||||
|
regionalAgentsNotSelectedMessage: string;
|
||||||
|
|
||||||
|
// ServiceUrlField.tsx
|
||||||
|
targetDefault: string;
|
||||||
|
targetDNS: string;
|
||||||
|
targetHTTPDesc: string;
|
||||||
|
targetPINGDesc: string;
|
||||||
|
targetTCPDesc: string;
|
||||||
|
targetTCPPortDesc: string;
|
||||||
|
targetDNSDesc: string;
|
||||||
|
targetDefaultDesc: string;
|
||||||
|
targetDefaultPlaceholder: string;
|
||||||
|
|
||||||
|
// types.ts
|
||||||
|
serviceNameRequired: string;
|
||||||
|
urlDomainHostRequired: string;
|
||||||
|
enterValidUrlHostnameDomain: string;
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
upServices: string;
|
||||||
|
downServices: string;
|
||||||
|
pausedServices: string;
|
||||||
|
warningServices: string;
|
||||||
|
|
||||||
|
// ServiceRowActions.tsx
|
||||||
|
viewDetail: string;
|
||||||
|
resumeMonitoring: string;
|
||||||
|
pauseMonitoring: string;
|
||||||
|
unmuteAlerts: string;
|
||||||
|
muteAlerts: string;
|
||||||
|
|
||||||
|
//IncidentTable.tsx
|
||||||
|
responseTime: string;
|
||||||
|
errorMessage: string;
|
||||||
|
details: string;
|
||||||
|
|
||||||
|
//LastCheckedTime.tsx
|
||||||
|
pausedAt: string;
|
||||||
|
lastCheckDetails: string;
|
||||||
|
checkedAt: string;
|
||||||
|
monitoringPaused: string;
|
||||||
|
noAutomaticChecks: string;
|
||||||
|
|
||||||
|
//ServiveEditDialog.tsx
|
||||||
|
editService: string;
|
||||||
|
editServiceDesc: string;
|
||||||
|
|
||||||
|
//ServiceStatsCards.tsx
|
||||||
|
lastCheckedAt: string;
|
||||||
|
avg: string;
|
||||||
|
lastUpChecksCount: string;
|
||||||
|
basedOnlastChecksCount: string;
|
||||||
|
totalUptime: string;
|
||||||
|
totalDowntime: string;
|
||||||
|
monitoringSettings: string;
|
||||||
|
monitoringSettingsInterval: string;
|
||||||
|
monitoringSettingsType: string;
|
||||||
|
upStatusDuration: string;
|
||||||
|
downStatusDuration: string;
|
||||||
|
|
||||||
|
incidentHistory: string;
|
||||||
|
processing: string;
|
||||||
|
back: string;
|
||||||
|
last20Checks: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
|
||||||
export interface SettingsTranslations {
|
export interface SettingsTranslations {
|
||||||
// Tabs
|
// General Settings - Tabs
|
||||||
systemSettings: string;
|
systemSettings: string;
|
||||||
mailSettings: string;
|
mailSettings: string;
|
||||||
|
|
||||||
// System Settings
|
// General Settings - System Settings
|
||||||
appName: string;
|
appName: string;
|
||||||
appURL: string;
|
appURL: string;
|
||||||
senderName: string;
|
senderName: string;
|
||||||
senderEmail: string;
|
senderEmail: string;
|
||||||
hideControls: string;
|
hideControls: string;
|
||||||
|
|
||||||
// Mail Settings
|
// General Settings - Mail Settings
|
||||||
smtpSettings?: string;
|
smtpSettings?: string;
|
||||||
smtpEnabled: string;
|
smtpEnabled: string;
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
@@ -21,8 +21,8 @@ export interface SettingsTranslations {
|
|||||||
smtpAuthMethod: string;
|
smtpAuthMethod: string;
|
||||||
enableTLS: string;
|
enableTLS: string;
|
||||||
localName: string;
|
localName: string;
|
||||||
|
|
||||||
// Test Email
|
// General Settings - Test Email
|
||||||
testEmail: string;
|
testEmail: string;
|
||||||
sendTestEmail: string;
|
sendTestEmail: string;
|
||||||
emailTemplate: string;
|
emailTemplate: string;
|
||||||
@@ -35,9 +35,13 @@ export interface SettingsTranslations {
|
|||||||
selectCollection: string;
|
selectCollection: string;
|
||||||
toEmailAddress: string;
|
toEmailAddress: string;
|
||||||
enterEmailAddress: string;
|
enterEmailAddress: string;
|
||||||
|
send: string;
|
||||||
sending: string;
|
sending: string;
|
||||||
|
testEmailSettings: string;
|
||||||
// Actions and status
|
testEmailDescription: string;
|
||||||
|
testEmailAlert: string;
|
||||||
|
|
||||||
|
// General Settings - Actions and status
|
||||||
save: string;
|
save: string;
|
||||||
saving: string;
|
saving: string;
|
||||||
settingsUpdated: string;
|
settingsUpdated: string;
|
||||||
@@ -47,4 +51,11 @@ export interface SettingsTranslations {
|
|||||||
testingConnection: string;
|
testingConnection: string;
|
||||||
connectionSuccess: string;
|
connectionSuccess: string;
|
||||||
connectionFailed: string;
|
connectionFailed: string;
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
addUser: string;
|
||||||
|
permissionNotice: string;
|
||||||
|
permissionNoticeAddUser: string;
|
||||||
|
loadingSettings: string;
|
||||||
|
loadingSettingsError: string;
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user