Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 250f5bd62e | |||
| c484ac5567 | |||
| e2b20fef2e | |||
| 03cead6102 | |||
| 5ac6f16fb5 | |||
| 681efc4383 | |||
| 8740f35728 | |||
| f5a985ff1e | |||
| 4b27b85038 | |||
| 463299fa73 | |||
| 91ceddc489 | |||
| f2c338ced9 | |||
| 9dbd267a22 | |||
| 2394243cb9 | |||
| bc38dba859 | |||
| 8f84fbf562 | |||
| 6b8ee741b3 | |||
| 370f613080 | |||
| 4f367b92da | |||
| 6ab3be6fe4 | |||
| 4221c4751a | |||
| a39b326f0e | |||
| 84916f705a | |||
| e4e87abac5 | |||
| f528f6eaea | |||
| b8966fe743 | |||
| 2a4a6cea48 | |||
| 3a3dc020fd | |||
| ed5cff5026 | |||
| bf45f896bd | |||
| 00430b6cb7 | |||
| feb65edee5 | |||
| 04f125ac1e | |||
| 435694f694 | |||
| c33328f977 | |||
| 17e0d0a794 | |||
| d408658b69 | |||
| 682ce84ef7 | |||
| 7392580db8 | |||
| ee5e66eb44 | |||
| ed220b9a27 | |||
| b6ef4ff7a5 | |||
| a4364936e3 | |||
| f5a8d989cc | |||
| 2e2df30b85 | |||
| 498b26dbd5 | |||
| 79fda8105f | |||
| 8d8e871b9f |
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
server/
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.next
|
||||
**/.turbo
|
||||
**/.cache
|
||||
**/coverage
|
||||
**/*.log
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Build and Publish Frontend Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "develop", "feature/**" ]
|
||||
paths:
|
||||
- 'application/**'
|
||||
- '.github/workflows/build-frontend-image.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/checkcle-frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: application
|
||||
file: application/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
fileignoreconfig:
|
||||
- filename: application/package-lock.json
|
||||
checksum: 3253a06ef27a634ec2b8a8fe9c156288b9fcf57024623c7ddc3d321f2568e498
|
||||
version: "1.0"
|
||||
+2
-2
@@ -79,7 +79,7 @@ Click "Fork" on [GitHub](https://github.com/operacle/checkcle) to create your ow
|
||||
|
||||
### 2. Clone Your Fork
|
||||
```bash
|
||||
git clone --branch develop https://github.com/operacle/checkcle.git
|
||||
git clone https://github.com/yourusername/checkcle.git
|
||||
cd checkcle
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ go run main.go (you do not need to change the .env while it's the localhost conn
|
||||
Click "Fork" on [GitHub](https://github.com/operacle/Distributed-Regional-Monitoring) to create your own copy.
|
||||
|
||||
#### 2. Clone Your Fork
|
||||
git clone --branch main https://github.com/operacle/Distributed-Regional-Monitoring.git
|
||||
git clone https://github.com/yourusername/Distributed-Regional-Monitoring.git
|
||||
cd Distributed-Regional-Monitoring
|
||||
|
||||
#### 3. Install Go Service (make sure you have installed the Go Service)
|
||||
|
||||
@@ -60,7 +60,7 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
|
||||
- Infrastructure Server Monitoring, Supports Linux (🐧 Debian, Ubuntu, CentOS, Red Hat, etc.) and Windows (Beta). And Servers metrics like CPU, RAM, disk usage, and network activity) with an one-line installation angent script.
|
||||
- Schedule Maintenance & Incident Management
|
||||
- Operational Status / Public Status Pages
|
||||
- Notifications via email, Telegram, Discord, and Slack
|
||||
- Notifications via email, Telegram, Discord, Slack, Matrix, and more
|
||||
- Reports & Analytics
|
||||
- Settings Panel (User Management, Data Retention, Multi-language, Themes (Dark & Light Mode), Notification and channels and alert templates).
|
||||
|
||||
@@ -151,6 +151,12 @@ If you’re a tech company interested in supporting CheckCle, please contact the
|
||||
height="50"
|
||||
alt="DigitalOcean Logo" />
|
||||
</a>
|
||||
</a>
|
||||
<a href="https://www.jetbrains.com/">
|
||||
<img src="https://cdn.checkcle.io/images/sponsor/jetbrains.png"
|
||||
height="50"
|
||||
alt="Jetbrains Logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
@@ -179,7 +185,7 @@ Here are some ways you can help improve CheckCle:
|
||||
- Website: [checkcle.io](https://checkcle.io)
|
||||
- Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle!
|
||||
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
|
||||
- Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss)
|
||||
- Follow us on X: [@asqrm_io](https://x.com/asqrm_io)
|
||||
|
||||
## 📜 License
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ We support the latest stable release of `checkcle`. Security patches may also be
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| `develop` (latest) | ✅ Yes |
|
||||
| `main` (latest) | ✅ Yes |
|
||||
| Older versions | ⚠️ Best-effort |
|
||||
| Pre-release or forks | ❌ No |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
node_modules
|
||||
dist
|
||||
.cache
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:20-bullseye-slim AS build
|
||||
WORKDIR /app
|
||||
ENV npm_config_fund=false \
|
||||
npm_config_audit=false \
|
||||
npm_config_progress=false
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --fund=false
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.npm npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
# SPA fallback + asset caching
|
||||
RUN printf 'server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n location ~* \\.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; }\n}\n' > /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx","-g","daemon off;"]
|
||||
Generated
+119
-7
@@ -47,7 +47,7 @@
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"pocketbase": "^0.19.0",
|
||||
"pocketbase": "^0.26.8",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -84,6 +84,7 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -728,6 +729,7 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
@@ -745,6 +747,7 @@
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
@@ -759,6 +762,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -768,6 +772,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -777,12 +782,14 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -793,6 +800,7 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -806,6 +814,7 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -815,6 +824,7 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -828,6 +838,7 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -2884,7 +2895,7 @@
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
@@ -2898,7 +2909,7 @@
|
||||
"version": "18.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
||||
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -2909,7 +2920,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
@@ -3208,6 +3219,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -3220,6 +3232,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -3235,12 +3248,14 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -3254,6 +3269,7 @@
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -3329,6 +3345,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
@@ -3345,6 +3362,7 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3368,6 +3386,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -3435,6 +3454,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3502,6 +3522,7 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
@@ -3526,6 +3547,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -3936,6 +3958,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -3948,12 +3971,14 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3982,6 +4007,7 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -4005,6 +4031,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
@@ -4191,12 +4218,14 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
@@ -4223,6 +4252,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
@@ -4264,6 +4294,7 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
@@ -4532,6 +4563,7 @@
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -4548,6 +4580,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -4574,6 +4607,7 @@
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -4602,6 +4636,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4652,6 +4687,7 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
@@ -4682,6 +4718,7 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -4696,6 +4733,7 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -4714,6 +4752,7 @@
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@@ -4734,6 +4773,7 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
@@ -4746,6 +4786,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -4755,6 +4796,7 @@
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -4800,6 +4842,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -4891,6 +4934,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
@@ -4903,6 +4947,7 @@
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
@@ -4918,6 +4963,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4927,6 +4973,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4936,6 +4983,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -4948,6 +4996,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -4957,12 +5006,14 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@@ -4978,6 +5029,7 @@
|
||||
"version": "1.21.6",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
|
||||
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -5078,6 +5130,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -5090,6 +5143,7 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
@@ -5149,6 +5203,7 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
@@ -5163,6 +5218,7 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -5172,6 +5228,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -5198,6 +5255,7 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -5214,6 +5272,7 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
@@ -5225,6 +5284,7 @@
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5267,6 +5327,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -5295,6 +5356,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -5354,6 +5416,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
@@ -5383,6 +5446,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5392,12 +5456,14 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -5421,12 +5487,14 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5439,6 +5507,7 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -5448,21 +5517,23 @@
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pocketbase": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.19.0.tgz",
|
||||
"integrity": "sha512-bUVZfVdD17K8GnwbeDMZPEdREVg2YE0F8uHPDC0zer4VtwXUqoPCCeudTy3fhUE7pfuKnfpuPxeBSYsBY3AGIQ==",
|
||||
"version": "0.26.8",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.8.tgz",
|
||||
"integrity": "sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5491,6 +5562,7 @@
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
@@ -5508,6 +5580,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelcase-css": "^2.0.1"
|
||||
@@ -5527,6 +5600,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
|
||||
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5562,6 +5636,7 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5587,6 +5662,7 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -5600,6 +5676,7 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
@@ -5643,6 +5720,7 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5877,6 +5955,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
@@ -5886,6 +5965,7 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
@@ -5937,6 +6017,7 @@
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
@@ -5964,6 +6045,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@@ -6020,6 +6102,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6065,6 +6148,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -6077,6 +6161,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6086,6 +6171,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -6108,6 +6194,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6127,6 +6214,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
@@ -6145,6 +6233,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -6159,6 +6248,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6168,12 +6258,14 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -6186,6 +6278,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -6202,6 +6295,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -6214,6 +6308,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6236,6 +6331,7 @@
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
@@ -6271,6 +6367,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -6303,6 +6400,7 @@
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -6366,6 +6464,7 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
@@ -6375,6 +6474,7 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
@@ -6393,6 +6493,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -6418,6 +6519,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
@@ -6572,6 +6674,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
@@ -6683,6 +6786,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -6708,6 +6812,7 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
@@ -6726,6 +6831,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -6743,6 +6849,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6752,12 +6859,14 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -6772,6 +6881,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -6784,6 +6894,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -6796,6 +6907,7 @@
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
||||
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"pocketbase": "^0.19.0",
|
||||
"pocketbase": "^0.26.8",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -82,4 +82,4 @@
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[
|
||||
{
|
||||
"id": "pbc_1938176441",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "alert_configurations",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -380,11 +380,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_2889059272",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "data_settings",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -478,11 +478,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_285611395",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "dns_data",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -717,11 +717,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_3168803865",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "docker_metrics",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -978,11 +978,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_583547467",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "dockers",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -1249,11 +1249,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_2189087560",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "incidents",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -1511,11 +1511,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1738231158",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "maintenance",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -1741,11 +1741,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_3662317132",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "operational_page",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -1932,11 +1932,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1168766540",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "ping_data",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -2199,11 +2199,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_171698555",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "regional_service",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -2331,11 +2331,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1998570700",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "server_metrics",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -2648,11 +2648,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1965843930",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "server_notification_templates",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -2948,11 +2948,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1464856393",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "server_threshold_templates",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -3094,11 +3094,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_3738798621",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "servers",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -3556,11 +3556,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_763794515",
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "service_group",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -3646,11 +3646,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1572233440",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "service_notification_templates",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -3806,11 +3806,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_863811952",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "services",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -4162,11 +4162,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_3575570325",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "services_metrics",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -4509,11 +4509,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1836745630",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "ssl_certificates",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -4841,11 +4841,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1753185970",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "ssl_history",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -5004,11 +5004,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_351174173",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "ssl_notification_templates",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -5122,11 +5122,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_1636150439",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "status_page_components",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -5252,11 +5252,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_2331916230",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "tcp_data",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -5449,11 +5449,11 @@
|
||||
},
|
||||
{
|
||||
"id": "pbc_67962705",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "@request.auth.id != \"\"",
|
||||
"deleteRule": "@request.auth.id != \"\"",
|
||||
"name": "uptime_data",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
@@ -5658,4 +5658,4 @@
|
||||
"indexes": [],
|
||||
"system": false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 250 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,75 +1,59 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AuthUser } from "@/services/authService";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react";
|
||||
import {
|
||||
Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText,
|
||||
Github, Twitter, MessageSquare, Bell, User, Settings,
|
||||
LogOut, Menu, X
|
||||
} from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuTrigger, DropdownMenuSeparator
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HeaderProps {
|
||||
currentUser: AuthUser | null;
|
||||
onLogout: () => void;
|
||||
sidebarCollapsed: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export const Header = ({
|
||||
currentUser,
|
||||
onLogout,
|
||||
sidebarCollapsed,
|
||||
toggleSidebar
|
||||
}: HeaderProps) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { language, setLanguage, t } = useLanguage();
|
||||
const { sidebarCollapsed, toggleSidebar, isMobileOpen, toggleMobileMenu } = useSidebar();
|
||||
const [greeting, setGreeting] = useState<string>("");
|
||||
const { systemName } = useSystemSettings();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Set greeting based on time of day
|
||||
useEffect(() => {
|
||||
const updateGreeting = () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 12) {
|
||||
setGreeting(t("goodMorning"));
|
||||
} else if (hour >= 12 && hour < 18) {
|
||||
setGreeting(t("goodAfternoon"));
|
||||
} else {
|
||||
setGreeting(t("goodEvening"));
|
||||
}
|
||||
if (hour >= 5 && hour < 12) setGreeting(t("goodMorning"));
|
||||
else if (hour >= 12 && hour < 18) setGreeting(t("goodAfternoon"));
|
||||
else setGreeting(t("goodEvening"));
|
||||
};
|
||||
updateGreeting();
|
||||
// Update greeting if language changes
|
||||
|
||||
// You could add a timer to update the greeting throughout the day
|
||||
// but that's typically unnecessary since most sessions aren't active across time periods
|
||||
}, [language, t]);
|
||||
|
||||
// Log avatar data for debugging
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
//console.log("Avatar URL in Header:", currentUser.avatar);
|
||||
}
|
||||
}, [currentUser]);
|
||||
const avatarUrl = currentUser?.avatar || '';
|
||||
|
||||
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
|
||||
let avatarUrl = '';
|
||||
if (currentUser?.avatar) {
|
||||
// If it's a relative path from the public folder, make sure it's resolved properly
|
||||
if (currentUser.avatar.startsWith('/upload/profile/')) {
|
||||
avatarUrl = currentUser.avatar;
|
||||
} else {
|
||||
avatarUrl = currentUser.avatar;
|
||||
}
|
||||
console.log("Final avatar URL:", avatarUrl);
|
||||
}
|
||||
const openExternalLink = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden">
|
||||
{/* Grid Pattern Overlay - Similar to StatusCards */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<header className="relative bg-background border-b border-border px-4 lg:px-6 flex justify-between items-center h-16 shrink-0 z-30 overflow-hidden">
|
||||
{/* Grid Pattern Overlay */}
|
||||
<div className="absolute inset-0 z-0 pointer-events-none opacity-20">
|
||||
<div
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
@@ -82,19 +66,74 @@ export const Header = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Content */}
|
||||
<div className="flex items-center gap-4 z-10">
|
||||
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
|
||||
<div className="flex items-center gap-2 lg:gap-4 z-10">
|
||||
{/* Mobile Toggle */}
|
||||
<Button variant="ghost" size="icon" onClick={toggleMobileMenu} className="lg:hidden h-9 w-9">
|
||||
{isMobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</Button>
|
||||
|
||||
{/* Desktop Toggle */}
|
||||
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="hidden lg:flex h-9 w-9">
|
||||
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 ✨</h1>
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-sm lg:text-lg font-medium truncate max-w-[150px] lg:max-w-none">
|
||||
{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 ✨
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 z-10">
|
||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}>
|
||||
<div className="flex items-center space-x-1 lg:space-x-3 z-10">
|
||||
{/* External Links - Hidden on small screens */}
|
||||
<div className="hidden md:flex items-center space-x-1 mr-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => openExternalLink('https://docs.checkcle.io')}
|
||||
title={t("documentation")}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => openExternalLink('https://github.com/operacle/checkcle')}
|
||||
title="GitHub"
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => openExternalLink('https://x.com/checkcle_oss')}
|
||||
title="X (Twitter)"
|
||||
>
|
||||
<Twitter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => openExternalLink('https://discord.gg/xs9gbubGwX')}
|
||||
title="Discord"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
title={t("notifications")}
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border hidden sm:flex" onClick={toggleTheme}>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
</Button>
|
||||
@@ -106,7 +145,7 @@ export const Header = ({
|
||||
<Globe className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
|
||||
{t("english")}
|
||||
</DropdownMenuItem>
|
||||
@@ -114,7 +153,7 @@ export const Header = ({
|
||||
{t("khmer")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
|
||||
{t("Deutsch")}
|
||||
{t("german")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
|
||||
{t("korean")}
|
||||
@@ -128,74 +167,21 @@ export const Header = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Documentation */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
|
||||
>
|
||||
<span className="sr-only">{t("documentation")}</span>
|
||||
<FileText className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* GitHub */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
|
||||
>
|
||||
<span className="sr-only">GitHub</span>
|
||||
<Github className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* X (Twitter) */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
|
||||
>
|
||||
<span className="sr-only">X (Twitter)</span>
|
||||
<Twitter className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Discord */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
|
||||
>
|
||||
<span className="sr-only">Discord</span>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
||||
<span className="sr-only">{t("notifications")}</span>
|
||||
<Bell className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* User Profile Dropdown */}
|
||||
<div className="h-8 w-px bg-border mx-1 hidden sm:block" />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
|
||||
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
|
||||
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
|
||||
</AvatarFallback>}
|
||||
<Avatar className="h-8 w-8 cursor-pointer border hover:ring-2 hover:ring-primary/20 transition-all">
|
||||
{avatarUrl ? <AvatarImage src={avatarUrl} alt="User" /> : <AvatarFallback className="bg-primary/20 text-primary">{currentUser?.name?.[0] || 'U'}</AvatarFallback>}
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<div className="flex items-center gap-3 p-2">
|
||||
<Avatar className="h-10 w-10">
|
||||
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
|
||||
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
|
||||
</AvatarFallback>}
|
||||
{avatarUrl ? <AvatarImage src={avatarUrl} alt="User" /> : <AvatarFallback className="bg-primary/20 text-primary">{currentUser?.name?.[0] || 'U'}</AvatarFallback>}
|
||||
</Avatar>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<span className="font-medium">{currentUser?.name || 'User'}</span>
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<span className="text-sm font-medium truncate">{currentUser?.name || 'User'}</span>
|
||||
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,4 +204,4 @@ export const Header = ({
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,28 +1,44 @@
|
||||
|
||||
import React from "react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { SidebarHeader } from "./sidebar/SidebarHeader";
|
||||
import { MainNavigation } from "./sidebar/MainNavigation";
|
||||
import { SettingsPanel } from "./sidebar/SettingsPanel";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export const Sidebar = ({ collapsed }: SidebarProps) => {
|
||||
export const Sidebar = () => {
|
||||
const { theme } = useTheme();
|
||||
const { sidebarCollapsed, isMobileOpen, toggleMobileMenu } = useSidebar();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
|
||||
border-r flex flex-col h-full
|
||||
${collapsed ? 'w-16' : 'w-64'}
|
||||
`}
|
||||
>
|
||||
<SidebarHeader collapsed={collapsed} />
|
||||
<MainNavigation collapsed={collapsed} />
|
||||
<SettingsPanel collapsed={collapsed} />
|
||||
</div>
|
||||
<>
|
||||
{/* Mobile Overlay */}
|
||||
{isMobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 z-40 lg:hidden backdrop-blur-sm transition-opacity animate-in fade-in"
|
||||
onClick={toggleMobileMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar Container */}
|
||||
<aside
|
||||
className={cn(
|
||||
"fixed inset-y-0 left-0 z-50 lg:static lg:block transition-all duration-300 ease-in-out border-r flex flex-col h-full",
|
||||
theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border',
|
||||
// Mobile state
|
||||
isMobileOpen ? "translate-x-0 w-64" : "-translate-x-full lg:translate-x-0",
|
||||
// Desktop state (collapsed/expanded)
|
||||
!isMobileOpen && sidebarCollapsed ? "lg:w-16" : "lg:w-64"
|
||||
)}
|
||||
>
|
||||
<SidebarHeader collapsed={!isMobileOpen && sidebarCollapsed} />
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden custom-scrollbar">
|
||||
<MainNavigation collapsed={!isMobileOpen && sidebarCollapsed} />
|
||||
</div>
|
||||
|
||||
<SettingsPanel collapsed={!isMobileOpen && sidebarCollapsed} />
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MenuItemProps {
|
||||
id: string;
|
||||
@@ -16,7 +16,6 @@ interface MenuItemProps {
|
||||
}
|
||||
|
||||
export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
id,
|
||||
path,
|
||||
icon: Icon,
|
||||
translationKey,
|
||||
@@ -29,36 +28,39 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isActive = path && location.pathname === path;
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasNavigation && path) {
|
||||
navigate(path, { replace: false });
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
|
||||
const isActive = path && location.pathname === path;
|
||||
const mainIconSize = "h-6 w-6";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
${collapsed ? 'p-3' : 'p-2 pl-3'}
|
||||
mb-1 rounded-lg
|
||||
${isActive ? (theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent') : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`}
|
||||
flex items-center
|
||||
${collapsed ? 'justify-center' : ''}
|
||||
cursor-pointer
|
||||
`}
|
||||
className={cn(
|
||||
"group relative flex items-center mb-1 rounded-lg cursor-pointer transition-all duration-200",
|
||||
collapsed ? "p-3 justify-center" : "p-2.5 px-4",
|
||||
isActive
|
||||
? (theme === 'dark' ? 'bg-zinc-800 text-white' : 'bg-primary/10 text-primary')
|
||||
: (theme === 'dark' ? 'text-zinc-400 hover:bg-zinc-800/50 hover:text-white' : 'text-zinc-600 hover:bg-zinc-100')
|
||||
)}
|
||||
onClick={handleClick}
|
||||
title={collapsed ? t(translationKey) : ""}
|
||||
>
|
||||
<Icon className={`${mainIconSize} ${color}`} />
|
||||
<Icon className={cn("h-5 w-5 shrink-0 transition-colors", color, isActive ? "opacity-100" : "opacity-80 group-hover:opacity-100")} />
|
||||
|
||||
{!collapsed && (
|
||||
<span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
|
||||
<span className="ml-3 font-medium tracking-wide text-sm truncate">
|
||||
{t(translationKey)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Small Indicator for active state */}
|
||||
{isActive && !collapsed && (
|
||||
<div className="absolute left-0 w-1 h-6 bg-primary rounded-r-full" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table, TableBody } from "@/components/ui/table";
|
||||
import { DockerContainer } from "@/types/docker.types";
|
||||
import { DockerMetricsDialog } from "./DockerMetricsDialog";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import {
|
||||
DockerTableSearch,
|
||||
DockerTableHeader,
|
||||
@@ -18,6 +19,7 @@ interface DockerContainersTableProps {
|
||||
}
|
||||
|
||||
export const DockerContainersTable = ({ containers, isLoading, onRefresh }: DockerContainersTableProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedContainer, setSelectedContainer] = useState<DockerContainer | null>(null);
|
||||
const [metricsDialogOpen, setMetricsDialogOpen] = useState(false);
|
||||
@@ -49,7 +51,7 @@ export const DockerContainersTable = ({ containers, isLoading, onRefresh }: Dock
|
||||
<CardHeader className="pb-4 px-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<CardTitle className="text-lg sm:text-xl font-semibold">Docker Containers</CardTitle>
|
||||
<CardTitle className="text-lg sm:text-xl font-semibold">{t('dockerContainers', 'docker')}</CardTitle>
|
||||
<DockerTableSearch
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DockerContainer, DockerMetrics } from "@/types/docker.types";
|
||||
import { dockerService } from "@/services/dockerService";
|
||||
import { Loader2, Cpu, HardDrive, Network, MemoryStick } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerMetricsDialogProps {
|
||||
container: DockerContainer | null;
|
||||
@@ -20,16 +21,17 @@ interface DockerMetricsDialogProps {
|
||||
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
|
||||
|
||||
const timeRangeOptions = [
|
||||
{ value: '60m' as TimeRange, label: '60 minutes', hours: 1 },
|
||||
{ value: '1d' as TimeRange, label: '1 day', hours: 24 },
|
||||
{ value: '7d' as TimeRange, label: '7 days', hours: 24 * 7 },
|
||||
{ value: '1m' as TimeRange, label: '1 month', hours: 24 * 30 },
|
||||
{ value: '3m' as TimeRange, label: '3 months', hours: 24 * 90 },
|
||||
{ value: '60m' as TimeRange, label: 'minutes60', hours: 1 },
|
||||
{ value: '1d' as TimeRange, label: 'day1', hours: 24 },
|
||||
{ value: '7d' as TimeRange, label: 'days7', hours: 24 * 7 },
|
||||
{ value: '1m' as TimeRange, label: 'month1', hours: 24 * 30 },
|
||||
{ value: '3m' as TimeRange, label: 'months3', hours: 24 * 90 },
|
||||
];
|
||||
|
||||
export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMetricsDialogProps) => {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const {
|
||||
data: metrics = [],
|
||||
@@ -232,11 +234,11 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<DialogContent className="max-w-7xl max-h-[95vh] overflow-y-auto bg-background text-foreground">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
Container Metrics: {container.name}
|
||||
{t('containerMetricsTitle', 'docker', { name: container.name })}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
|
||||
@@ -246,7 +248,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<SelectContent>
|
||||
{timeRangeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{t(option.label, 'docker')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -254,22 +256,22 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Docker ID: {container.docker_id} • {container.hostname}
|
||||
{t('dockerId', 'docker')}: {container.docker_id} • {container.hostname}
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">Loading metrics...</span>
|
||||
<span className="ml-2">{t('loadingMetrics', 'docker')}</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center h-96 text-muted-foreground">
|
||||
<p>Error loading metrics: {error.message}</p>
|
||||
<p>{t('errorLoadingMetrics', 'docker')}: {String((error as any)?.message ?? '')}</p>
|
||||
</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-96 text-muted-foreground">
|
||||
<p>No metrics data available for this container</p>
|
||||
<p>{t('noMetricsAvailable', 'docker')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -277,7 +279,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
{latestMetric && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<MetricCard
|
||||
title="CPU"
|
||||
title={t('cpu', 'docker')}
|
||||
used={`${latestMetric.cpuUsage}%`}
|
||||
total={`${latestMetric.cpuCores} cores`}
|
||||
free={`${(100 - latestMetric.cpuUsage).toFixed(1)}%`}
|
||||
@@ -286,7 +288,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
color={chartConfig.cpuUsage.color}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Memory"
|
||||
title={t('memoryTitle', 'docker')}
|
||||
used={latestMetric.ramUsed}
|
||||
total={latestMetric.ramTotal}
|
||||
free={latestMetric.ramFree}
|
||||
@@ -295,7 +297,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
color={chartConfig.ramUsagePercent.color}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Disk"
|
||||
title={t('diskTitle', 'docker')}
|
||||
used={latestMetric.diskUsed}
|
||||
total={latestMetric.diskTotal}
|
||||
free={latestMetric.diskFree}
|
||||
@@ -304,10 +306,10 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
color={chartConfig.diskUsagePercent.color}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Network"
|
||||
title={t('network', 'docker')}
|
||||
used={`RX: ${latestMetric.networkRx}`}
|
||||
total={`TX: ${latestMetric.networkTx}`}
|
||||
free={`Speed: ${latestMetric.networkRxSpeed} KB/s`}
|
||||
free={`${t('networkSpeedKbs', 'docker').replace('(KB/s)', '')}: ${latestMetric.networkRxSpeed} KB/s`}
|
||||
percentage={0}
|
||||
icon={Network}
|
||||
color={chartConfig.networkRx.color}
|
||||
@@ -319,19 +321,19 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<TabsList className="grid w-full grid-cols-4 bg-muted">
|
||||
<TabsTrigger value="cpu" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<Cpu className="h-4 w-4" />
|
||||
CPU
|
||||
{t('cpu', 'docker')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="memory" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<MemoryStick className="h-4 w-4" />
|
||||
Memory
|
||||
{t('memoryTitle', 'docker')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="disk" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Disk
|
||||
{t('diskTitle', 'docker')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="network" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<Network className="h-4 w-4" />
|
||||
Network
|
||||
{t('network', 'docker')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -339,7 +341,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">CPU Usage (%)</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('cpuUsagePct', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
@@ -365,7 +367,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={chartConfig.cpuUsage.color}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, fill: chartConfig.cpuUsage.color }}
|
||||
name="CPU Usage (%)"
|
||||
name={t('cpuUsagePct', 'docker')}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
@@ -374,7 +376,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">CPU Usage vs Available</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('cpuUsageVsAvailable', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
@@ -401,7 +403,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fillOpacity={0.3}
|
||||
name="CPU Available (%)"
|
||||
name={t('cpuAvailablePct', 'docker')}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
@@ -410,7 +412,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={chartConfig.cpuUsage.color}
|
||||
fill={chartConfig.cpuUsage.color}
|
||||
fillOpacity={0.6}
|
||||
name="CPU Usage (%)"
|
||||
name={t('cpuUsagePct', 'docker')}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
@@ -423,7 +425,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Memory Usage (%)</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('ramUsagePct', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
@@ -459,7 +461,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Memory Usage (Bytes)</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('memoryUsageBytes', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
@@ -479,8 +481,8 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
formatter={(value, name) => [
|
||||
name === 'Used Memory' ? formatBytes(Number(value)) :
|
||||
name === 'Total Memory' ? formatBytes(Number(value)) : value,
|
||||
name === t('usedMemory', 'docker') ? formatBytes(Number(value)) :
|
||||
name === t('totalMemory', 'docker') ? formatBytes(Number(value)) : value,
|
||||
name
|
||||
]}
|
||||
/>
|
||||
@@ -491,7 +493,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fillOpacity={0.3}
|
||||
name="Total Memory"
|
||||
name={t('totalMemory', 'docker')}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
@@ -500,7 +502,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={chartConfig.ramUsagePercent.color}
|
||||
fill={chartConfig.ramUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="Used Memory"
|
||||
name={t('usedMemory', 'docker')}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
@@ -513,7 +515,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Disk Usage (%)</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('diskUsagePct', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
@@ -540,7 +542,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={chartConfig.diskUsagePercent.color}
|
||||
fill={chartConfig.diskUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="Disk Usage (%)"
|
||||
name={t('diskUsagePct', 'docker')}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
@@ -549,7 +551,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Disk Usage (Bytes)</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('diskUsageBytes', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
@@ -569,8 +571,8 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
formatter={(value, name) => [
|
||||
name === 'Used Disk' ? formatBytes(Number(value)) :
|
||||
name === 'Total Disk' ? formatBytes(Number(value)) : value,
|
||||
name === t('usedDisk', 'docker') ? formatBytes(Number(value)) :
|
||||
name === t('totalDisk', 'docker') ? formatBytes(Number(value)) : value,
|
||||
name
|
||||
]}
|
||||
/>
|
||||
@@ -581,7 +583,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fillOpacity={0.3}
|
||||
name="Total Disk"
|
||||
name={t('totalDisk', 'docker')}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
@@ -590,7 +592,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
stroke={chartConfig.diskUsagePercent.color}
|
||||
fill={chartConfig.diskUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="Used Disk"
|
||||
name={t('usedDisk', 'docker')}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
@@ -603,7 +605,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Network Traffic</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('networkTraffic', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-64">
|
||||
@@ -627,7 +629,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
dataKey="networkRxBytes"
|
||||
stroke={chartConfig.networkRx.color}
|
||||
strokeWidth={2}
|
||||
name="RX Bytes"
|
||||
name={t('rxBytes', 'docker')}
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
<Line
|
||||
@@ -635,7 +637,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
dataKey="networkTxBytes"
|
||||
stroke={chartConfig.networkTx.color}
|
||||
strokeWidth={2}
|
||||
name="TX Bytes"
|
||||
name={t('txBytes', 'docker')}
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
@@ -645,7 +647,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Network Speed (KB/s)</CardTitle>
|
||||
<CardTitle className="text-foreground">{t('networkSpeedKbs', 'docker')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-64">
|
||||
@@ -669,7 +671,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
dataKey="networkRxSpeed"
|
||||
stroke={chartConfig.networkRx.color}
|
||||
strokeWidth={2}
|
||||
name="RX Speed (KB/s)"
|
||||
name={t('rxSpeedKbs', 'docker')}
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
<Line
|
||||
@@ -677,7 +679,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
|
||||
dataKey="networkTxSpeed"
|
||||
stroke={chartConfig.networkTx.color}
|
||||
strokeWidth={2}
|
||||
name="TX Speed (KB/s)"
|
||||
name={t('txSpeedKbs', 'docker')}
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Container, Play, Square, AlertTriangle } from "lucide-react";
|
||||
import { DockerStats } from "@/types/docker.types";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerStatsCardsProps {
|
||||
stats: DockerStats;
|
||||
@@ -11,10 +12,11 @@ interface DockerStatsCardsProps {
|
||||
|
||||
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "Total Containers",
|
||||
title: t('totalContainers', 'docker'),
|
||||
value: stats.total,
|
||||
icon: Container,
|
||||
color: "text-blue-600",
|
||||
@@ -23,7 +25,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)"
|
||||
},
|
||||
{
|
||||
title: "Running",
|
||||
title: t('running', 'docker'),
|
||||
value: stats.running,
|
||||
icon: Play,
|
||||
color: "text-green-600",
|
||||
@@ -32,7 +34,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)"
|
||||
},
|
||||
{
|
||||
title: "Stopped",
|
||||
title: t('stopped', 'docker'),
|
||||
value: stats.stopped,
|
||||
icon: Square,
|
||||
color: "text-gray-600",
|
||||
@@ -41,7 +43,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)"
|
||||
},
|
||||
{
|
||||
title: "Warning",
|
||||
title: t('warning', 'docker'),
|
||||
value: stats.warning,
|
||||
icon: AlertTriangle,
|
||||
color: "text-amber-600",
|
||||
@@ -90,7 +92,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
variant="outline"
|
||||
className="text-xs font-mono font-bold px-2 py-1 rounded-md bg-white/20 backdrop-blur-sm text-white border border-white/30"
|
||||
>
|
||||
Containers
|
||||
{t('containersLabel', 'docker')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerStatusBadgeProps {
|
||||
status: 'running' | 'stopped' | 'warning';
|
||||
}
|
||||
|
||||
export const DockerStatusBadge = ({ status }: DockerStatusBadgeProps) => {
|
||||
const { t } = useLanguage();
|
||||
const getStatusConfig = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return {
|
||||
variant: 'default' as const,
|
||||
className: 'bg-emerald-100 text-emerald-800 border-emerald-200 hover:bg-emerald-200',
|
||||
label: 'Running'
|
||||
label: t('running', 'docker')
|
||||
};
|
||||
case 'stopped':
|
||||
return {
|
||||
variant: 'secondary' as const,
|
||||
className: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200',
|
||||
label: 'Stopped'
|
||||
label: t('stopped', 'docker')
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
variant: 'destructive' as const,
|
||||
className: 'bg-amber-100 text-amber-800 border-amber-200 hover:bg-amber-200',
|
||||
label: 'Warning'
|
||||
label: t('warning', 'docker')
|
||||
};
|
||||
default:
|
||||
return {
|
||||
variant: 'outline' as const,
|
||||
className: 'bg-gray-100 text-gray-600 border-gray-200',
|
||||
label: 'Unknown'
|
||||
label: t('unknown', 'docker')
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerEmptyStateProps {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12 text-muted-foreground">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="text-lg font-medium">
|
||||
{searchTerm ? "No containers found" : "No containers running"}
|
||||
{searchTerm ? t('noContainersFound', 'docker') : t('noContainersRunning', 'docker')}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."}
|
||||
{searchTerm ? t('tryAdjustSearch', 'docker') : t('startSomeContainers', 'docker')}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Eye, Play, Pause, Square, Trash2, BarChart3, RefreshCw } from "lucide-react";
|
||||
import { DockerContainer } from "@/types/docker.types";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerRowActionsProps {
|
||||
container: DockerContainer;
|
||||
@@ -12,11 +13,12 @@ interface DockerRowActionsProps {
|
||||
}
|
||||
|
||||
export const DockerRowActions = ({ container, containerStatus, onContainerAction, onViewMetrics }: DockerRowActionsProps) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-muted">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<span className="sr-only">{t('openMenu', 'docker')}</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -26,14 +28,14 @@ export const DockerRowActions = ({ container, containerStatus, onContainerAction
|
||||
className="cursor-pointer hover:bg-muted"
|
||||
>
|
||||
<BarChart3 className="mr-2 h-4 w-4" />
|
||||
View Metrics
|
||||
{t('viewMetrics', 'docker')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContainerAction('view-detail', container.id, container.name)}
|
||||
className="cursor-pointer hover:bg-muted"
|
||||
>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Details
|
||||
{t('viewDetails', 'docker')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
|
||||
import { TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export const DockerTableHeader = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<TableHeader>
|
||||
<TableRow className="border-border bg-muted/30">
|
||||
<TableHead className="min-w-[200px] font-semibold">Container</TableHead>
|
||||
<TableHead className="min-w-[100px] font-semibold">Status</TableHead>
|
||||
<TableHead className="min-w-[140px] font-semibold">CPU Usage</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">Memory</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">Disk</TableHead>
|
||||
<TableHead className="min-w-[100px] font-semibold">Uptime</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">Last Checked</TableHead>
|
||||
<TableHead className="min-w-[80px] text-center font-semibold">Actions</TableHead>
|
||||
<TableHead className="min-w-[200px] font-semibold">{t('container', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[100px] font-semibold">{t('status', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[140px] font-semibold">{t('cpuUsage', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">{t('memory', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">{t('disk', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[100px] font-semibold">{t('uptime', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">{t('lastChecked', 'docker')}</TableHead>
|
||||
<TableHead className="min-w-[80px] text-center font-semibold">{t('actions', 'docker')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, RefreshCw } from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerTableSearchProps {
|
||||
searchTerm: string;
|
||||
@@ -11,12 +12,13 @@ interface DockerTableSearchProps {
|
||||
}
|
||||
|
||||
export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoading }: DockerTableSearchProps) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
|
||||
<div className="relative flex-1 sm:flex-initial">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search containers..."
|
||||
placeholder={t('searchContainersPlaceholder', 'docker')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10 sm:w-64 bg-background border-border"
|
||||
@@ -30,7 +32,7 @@ export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoa
|
||||
className="w-full sm:w-auto bg-background border-border hover:bg-muted"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<span className="sm:inline">Refresh</span>
|
||||
<span className="sm:inline">{t('refresh', 'docker')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Plus, X, Server } from 'lucide-react';
|
||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { serviceService } from '@/services/serviceService';
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ComponentsSelectorProps {
|
||||
selectedComponents: Partial<StatusPageComponentRecord>[];
|
||||
@@ -19,6 +20,7 @@ interface ComponentsSelectorProps {
|
||||
}
|
||||
|
||||
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newComponent, setNewComponent] = useState({
|
||||
name: '',
|
||||
@@ -71,16 +73,16 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5" />
|
||||
Status Page Components
|
||||
{t('statusPageComponents')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add monitoring components like uptime services, SSL certificates, and incident tracking
|
||||
{t('addMonitoringComponentsDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{selectedComponents.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>Selected Components</Label>
|
||||
<Label>{t('selectedComponents')}</Label>
|
||||
<div className="space-y-2">
|
||||
{selectedComponents.map((component, index) => (
|
||||
<div key={component.id || index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
@@ -92,12 +94,12 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
<div className="flex gap-2 mt-1">
|
||||
{component.service_id && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Service: {services.find(s => s.id === component.service_id)?.name || component.service_id}
|
||||
{t('service')}: {services.find(s => s.id === component.service_id)?.name || component.service_id}
|
||||
</Badge>
|
||||
)}
|
||||
{component.server_id && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Server: {component.server_id}
|
||||
{t('server')}: {component.server_id}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -122,22 +124,22 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Component
|
||||
{t('addComponent')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="border rounded-lg p-4 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="component-name">Component Name</Label>
|
||||
<Label htmlFor="component-name">{t('componentName')}</Label>
|
||||
<Input
|
||||
id="component-name"
|
||||
placeholder="e.g., Main Website"
|
||||
placeholder={t('componentNamePlaceholder')}
|
||||
value={newComponent.name}
|
||||
onChange={(e) => setNewComponent({ ...newComponent, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="display-order">Display Order</Label>
|
||||
<Label htmlFor="display-order">{t('displayOrder')}</Label>
|
||||
<Input
|
||||
id="display-order"
|
||||
type="number"
|
||||
@@ -148,10 +150,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="component-description">Description (Optional)</Label>
|
||||
<Label htmlFor="component-description">{t('descriptionOptional')}</Label>
|
||||
<Textarea
|
||||
id="component-description"
|
||||
placeholder="Brief description of this component"
|
||||
placeholder={t('descriptionPlaceholder')}
|
||||
value={newComponent.description}
|
||||
onChange={(e) => setNewComponent({ ...newComponent, description: e.target.value })}
|
||||
/>
|
||||
@@ -159,10 +161,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="service-id">Uptime Service (Optional)</Label>
|
||||
<Label htmlFor="service-id">{t('uptimeServiceOptional')}</Label>
|
||||
<Select onValueChange={(value) => setNewComponent({ ...newComponent, service_id: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an uptime service" />
|
||||
<SelectValue placeholder={t('selectUptimeService')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{services.map((service) => (
|
||||
@@ -181,10 +183,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="server-id">Server ID (Optional)</Label>
|
||||
<Label htmlFor="server-id">{t('serverIdOptional')}</Label>
|
||||
<Input
|
||||
id="server-id"
|
||||
placeholder="server_456"
|
||||
placeholder={t('serverIdPlaceholder')}
|
||||
value={newComponent.server_id}
|
||||
onChange={(e) => setNewComponent({ ...newComponent, server_id: e.target.value })}
|
||||
/>
|
||||
@@ -193,10 +195,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={addComponent} disabled={!newComponent.name.trim()}>
|
||||
Add Component
|
||||
{t('addComponent')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShowAddForm(false)}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useCreateStatusPageComponent } from '@/hooks/useStatusPageComponents';
|
||||
import { ComponentsSelector } from './ComponentsSelector';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
@@ -31,6 +32,7 @@ const formSchema = z.object({
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const CreateOperationalPageDialog = () => {
|
||||
const { t } = useLanguage();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
|
||||
const createMutation = useCreateOperationalPage();
|
||||
@@ -102,14 +104,14 @@ export const CreateOperationalPageDialog = () => {
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Page
|
||||
{t('createPage')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Operational Page</DialogTitle>
|
||||
<DialogTitle>{t('createOperationalPage')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new operational status page to monitor your services and components.
|
||||
{t('createOperationalPageDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -121,9 +123,9 @@ export const CreateOperationalPageDialog = () => {
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormLabel>{t('title')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My Service Status" {...field} />
|
||||
<Input placeholder={t('myServiceStatusPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -135,9 +137,9 @@ export const CreateOperationalPageDialog = () => {
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormLabel>{t('slug')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-service-status" {...field} />
|
||||
<Input placeholder={t('myServiceStatusSlugPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -150,10 +152,10 @@ export const CreateOperationalPageDialog = () => {
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t('description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="A brief description of your operational page"
|
||||
placeholder={t('operationalPageDescriptionPlaceholder')}
|
||||
className="min-h-[80px]"
|
||||
{...field}
|
||||
/>
|
||||
@@ -169,17 +171,17 @@ export const CreateOperationalPageDialog = () => {
|
||||
name="theme"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<FormLabel>{t('theme')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select theme" />
|
||||
<SelectValue placeholder={t('selectTheme')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="default">{t('themeDefault')}</SelectItem>
|
||||
<SelectItem value="dark">{t('themeDark')}</SelectItem>
|
||||
<SelectItem value="light">{t('themeLight')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -192,18 +194,18 @@ export const CreateOperationalPageDialog = () => {
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Initial Status</FormLabel>
|
||||
<FormLabel>{t('initialStatus')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
<SelectValue placeholder={t('selectStatus')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="operational">Operational</SelectItem>
|
||||
<SelectItem value="degraded">Degraded Performance</SelectItem>
|
||||
<SelectItem value="maintenance">Under Maintenance</SelectItem>
|
||||
<SelectItem value="major_outage">Major Outage</SelectItem>
|
||||
<SelectItem value="operational">{t('statusOperational')}</SelectItem>
|
||||
<SelectItem value="degraded">{t('statusDegraded')}</SelectItem>
|
||||
<SelectItem value="maintenance">{t('statusMaintenance')}</SelectItem>
|
||||
<SelectItem value="major_outage">{t('statusMajorOutage')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -218,9 +220,9 @@ export const CreateOperationalPageDialog = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Public Page</FormLabel>
|
||||
<FormLabel>{t('publicPage')}</FormLabel>
|
||||
<FormDescription>
|
||||
Make this page publicly accessible
|
||||
{t('makePagePublic')}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -238,12 +240,12 @@ export const CreateOperationalPageDialog = () => {
|
||||
name="custom_domain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Custom Domain (Optional)</FormLabel>
|
||||
<FormLabel>{t('customDomainOptional')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="status.yourdomain.com" {...field} />
|
||||
<Input placeholder={t('customDomainPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Custom domain for your status page
|
||||
{t('customDomainDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -261,13 +263,13 @@ export const CreateOperationalPageDialog = () => {
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || createComponentMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending || createComponentMutation.isPending ? 'Creating...' : 'Create Page'}
|
||||
{createMutation.isPending || createComponentMutation.isPending ? t('creating') : t('createPage')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useCreateStatusPageComponent, useStatusPageComponentsByOperationalId, u
|
||||
import { ComponentsSelector } from './ComponentsSelector';
|
||||
import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
@@ -37,6 +38,7 @@ interface EditOperationalPageDialogProps {
|
||||
}
|
||||
|
||||
export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOperationalPageDialogProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
|
||||
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
|
||||
const [componentsLoaded, setComponentsLoaded] = useState(false);
|
||||
@@ -195,9 +197,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Operational Page</DialogTitle>
|
||||
<DialogTitle>{t('editOperationalPage')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update your operational status page settings and manage components.
|
||||
{t('updateYourOperationalPage')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -209,9 +211,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormLabel>{t('title')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My Service Status" {...field} />
|
||||
<Input placeholder={t('myServiceStatusPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -223,9 +225,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormLabel>{t('slug')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-service-status" {...field} />
|
||||
<Input placeholder={t('myServiceStatusSlugPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -238,10 +240,10 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t('description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="A brief description of your operational page"
|
||||
placeholder={t('operationalPageDescriptionPlaceholder')}
|
||||
className="min-h-[80px]"
|
||||
{...field}
|
||||
/>
|
||||
@@ -257,17 +259,17 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
name="theme"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<FormLabel>{t('theme')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select theme" />
|
||||
<SelectValue placeholder={t('selectTheme')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="default">{t('themeDefault')}</SelectItem>
|
||||
<SelectItem value="dark">{t('themeDark')}</SelectItem>
|
||||
<SelectItem value="light">{t('themeLight')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -280,18 +282,18 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<FormLabel>{t('status')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
<SelectValue placeholder={t('selectStatus')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="operational">Operational</SelectItem>
|
||||
<SelectItem value="degraded">Degraded Performance</SelectItem>
|
||||
<SelectItem value="maintenance">Under Maintenance</SelectItem>
|
||||
<SelectItem value="major_outage">Major Outage</SelectItem>
|
||||
<SelectItem value="operational">{t('statusOperational')}</SelectItem>
|
||||
<SelectItem value="degraded">{t('statusDegraded')}</SelectItem>
|
||||
<SelectItem value="maintenance">{t('statusMaintenance')}</SelectItem>
|
||||
<SelectItem value="major_outage">{t('statusMajorOutage')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -306,9 +308,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Public Page</FormLabel>
|
||||
<FormLabel>{t('publicPage')}</FormLabel>
|
||||
<FormDescription>
|
||||
Make this page publicly accessible
|
||||
{t('makePagePublic')}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -326,12 +328,12 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
name="custom_domain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Custom Domain (Optional)</FormLabel>
|
||||
<FormLabel>{t('customDomainOptional')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="status.yourdomain.com" {...field} />
|
||||
<Input placeholder={t('customDomainPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Custom domain for your status page
|
||||
{t('customDomainDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -350,13 +352,13 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending}
|
||||
>
|
||||
{isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending ? 'Updating...' : 'Update Page'}
|
||||
{isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending ? t('updating') : t('updatePage')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { Globe, ExternalLink, Eye, Settings, Trash2 } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface OperationalPageCardProps {
|
||||
page: OperationalPageRecord;
|
||||
@@ -15,6 +16,8 @@ interface OperationalPageCardProps {
|
||||
}
|
||||
|
||||
export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: OperationalPageCardProps) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
||||
<CardHeader className="pb-3">
|
||||
@@ -32,23 +35,23 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Slug:</span>
|
||||
<span className="font-medium text-muted-foreground">{t('slug')}:</span>
|
||||
<p className="mt-1">{page.slug}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Theme:</span>
|
||||
<span className="font-medium text-muted-foreground">{t('theme')}:</span>
|
||||
<p className="mt-1 capitalize">{page.theme}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Public:</span>
|
||||
<span className="font-medium text-muted-foreground">{t('public')}:</span>
|
||||
<div className="mt-1">
|
||||
<Badge variant={page.is_public === 'true' ? 'default' : 'secondary'}>
|
||||
{page.is_public === 'true' ? 'Yes' : 'No'}
|
||||
{page.is_public === 'true' ? t('yes') : t('no')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Updated:</span>
|
||||
<span className="font-medium text-muted-foreground">{t('updated')}:</span>
|
||||
<p className="mt-1">{format(new Date(page.updated), 'MMM dd, yyyy')}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +73,7 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
|
||||
className="flex-1"
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
View
|
||||
{t('view')}
|
||||
</Button>
|
||||
)}
|
||||
{onEdit && (
|
||||
@@ -81,7 +84,7 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
|
||||
className="flex-1"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
{t('edit')}
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Button } from '@/components/ui/button';
|
||||
import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { Activity, Plus, RefreshCw } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
export const OperationalPageContent = () => {
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
|
||||
const deleteMutation = useDeleteOperationalPage();
|
||||
@@ -68,13 +71,13 @@ export const OperationalPageContent = () => {
|
||||
<div className="mb-4">
|
||||
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('failedToLoadOperationalPages')}</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
There was an error loading your operational pages. Please try again.
|
||||
{t('loadingoperationalPages')}
|
||||
</p>
|
||||
<Button onClick={() => refetch()} variant="outline">
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
{t('tryagain')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,9 +89,9 @@ export const OperationalPageContent = () => {
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2"> {t('operationalPages')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your public status pages and monitor service health
|
||||
{t('describeOperation')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +103,7 @@ export const OperationalPageContent = () => {
|
||||
disabled={isRefetching}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<CreateOperationalPageDialog />
|
||||
</div>
|
||||
@@ -135,9 +138,9 @@ export const OperationalPageContent = () => {
|
||||
<div className="mb-4">
|
||||
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('noOperationalPagesFound')}</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Create your first operational page to start monitoring your services and communicate status to your users.
|
||||
{t('createYourFirstOperationalPage')}
|
||||
</p>
|
||||
<CreateOperationalPageDialog />
|
||||
</CardContent>
|
||||
@@ -164,14 +167,14 @@ export const OperationalPageContent = () => {
|
||||
<div className="mt-8 pt-6 border-t">
|
||||
<div className="flex flex-wrap gap-6 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<span className="font-medium">Total Pages:</span> {pages.length}
|
||||
<span className="font-medium">{t('totalPages')}:</span> {pages.length}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Public Pages:</span>{' '}
|
||||
<span className="font-medium">{t('totalPages')}:</span>{' '}
|
||||
{pages.filter(p => p.is_public === 'true').length}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Operational:</span>{' '}
|
||||
<span className="font-medium">{t('operational')}:</span>{' '}
|
||||
{pages.filter(p => p.status === 'operational').length}
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,19 +192,19 @@ export const OperationalPageContent = () => {
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteOperationalPage')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone and will permanently remove the operational page and all its components.
|
||||
{t('deleteOperationalPageConfirm').replace('{title}', pageToDelete?.title ?? '')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
{deleteMutation.isPending ? t('deleting') : t('delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { format } from 'date-fns';
|
||||
import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||
import { Service } from '@/types/service.types';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
interface CurrentStatusSectionProps {
|
||||
page: OperationalPageRecord;
|
||||
@@ -45,18 +46,18 @@ const getActualStatus = (components: StatusPageComponentRecord[], services: Serv
|
||||
return 'operational';
|
||||
};
|
||||
|
||||
const getStatusMessage = (status: OperationalPageRecord['status']) => {
|
||||
const getStatusMessage = (status: OperationalPageRecord['status'], t: (k: string, m?: string) => string) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return 'All systems are operational';
|
||||
return t('allOperational', 'public');
|
||||
case 'degraded':
|
||||
return 'Some systems are experiencing degraded performance';
|
||||
return t('degradedPerformance', 'public');
|
||||
case 'maintenance':
|
||||
return 'Systems are currently under maintenance';
|
||||
return t('underMaintenance', 'public');
|
||||
case 'major_outage':
|
||||
return 'We are experiencing a major service outage';
|
||||
return t('majorOutage', 'public');
|
||||
default:
|
||||
return 'Status unknown';
|
||||
return t('statusUnknown', 'public');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,6 +107,7 @@ const getStatusBackground = (status: OperationalPageRecord['status']) => {
|
||||
};
|
||||
|
||||
export const CurrentStatusSection = ({ page, components, services }: CurrentStatusSectionProps) => {
|
||||
const { t } = useLanguage();
|
||||
const actualStatus = getActualStatus(components, services);
|
||||
const displayStatus = actualStatus; // Use actual status for real-time accuracy
|
||||
|
||||
@@ -114,7 +116,7 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3 text-card-foreground text-xl">
|
||||
<Shield className="h-6 w-6" />
|
||||
System Status
|
||||
{t('systemStatus', 'public')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -123,10 +125,10 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
|
||||
{getStatusIcon(displayStatus)}
|
||||
<div>
|
||||
<h3 className={`text-2xl font-bold ${getStatusColor(displayStatus)}`}>
|
||||
{getStatusMessage(displayStatus)}
|
||||
{getStatusMessage(displayStatus, t)}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Status automatically updated based on component health
|
||||
{t('autoUpdatedByHealth', 'public')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,20 +138,20 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
|
||||
displayStatus === 'maintenance' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
|
||||
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
}`}>
|
||||
{displayStatus === 'operational' ? 'All Systems Operational' :
|
||||
displayStatus === 'degraded' ? 'Degraded Performance' :
|
||||
displayStatus === 'maintenance' ? 'Under Maintenance' : 'Major Outage'}
|
||||
{displayStatus === 'operational' ? t('allOperational', 'public') :
|
||||
displayStatus === 'degraded' ? t('degradedPerformance', 'public') :
|
||||
displayStatus === 'maintenance' ? t('underMaintenance', 'public') : t('majorOutage', 'public')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground border-t pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Last updated: {format(new Date(), 'MMM dd, yyyy HH:mm')} UTC</span>
|
||||
<span>{t('lastUpdatedAt', 'public', { time: format(new Date(), 'MMM dd, yyyy HH:mm') })}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>Live status monitoring</span>
|
||||
<span>{t('liveStatusMonitoring', 'public')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -9,8 +9,10 @@ import { CurrentStatusSection } from './CurrentStatusSection';
|
||||
import { ComponentsStatusSection } from './ComponentsStatusSection';
|
||||
import { OverallUptimeSection } from './OverallUptimeSection';
|
||||
import { PublicStatusPageFooter } from './PublicStatusPageFooter';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
|
||||
export const PublicStatusPage = () => {
|
||||
const { t } = useLanguage();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
// console.log('PublicStatusPage - slug from params:', slug);
|
||||
|
||||
@@ -59,9 +61,9 @@ export const PublicStatusPage = () => {
|
||||
<div className="text-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-lg font-medium text-foreground">Loading Status Page</p>
|
||||
<p className="text-sm text-muted-foreground">Fetching real-time system status...</p>
|
||||
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
|
||||
<p className="text-lg font-medium text-foreground">{t('loadingStatusPage', 'public')}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('fetchingRealtimeStatus', 'public')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('slugLabel', 'public')}: {slug || 'No slug provided'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,19 +78,19 @@ export const PublicStatusPage = () => {
|
||||
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-foreground">Status Page Not Found</h1>
|
||||
<h1 className="text-2xl font-bold text-foreground">{t('statusPageNotFound', 'public')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{error || 'The requested status page could not be found or is not publicly accessible.'}
|
||||
{error || t('notFoundDescription', 'public')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('slugLabel', 'public')}: {slug || 'No slug provided'}</p>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button onClick={() => window.history.back()} variant="outline">
|
||||
Go Back
|
||||
{t('goBack', 'public')}
|
||||
</Button>
|
||||
<Button onClick={() => window.location.reload()} className="gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Retry
|
||||
{t('retry', 'public')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Copy, Download, Terminal, CheckCircle, Zap, Play } from "lucide-react";
|
||||
import { regionalService } from "@/services/regionalService";
|
||||
import { InstallCommand } from "@/types/regional.types";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { getCurrentEndpoint } from "@/lib/pocketbase";
|
||||
import { RegionalAgentConfigForm } from "./RegionalAgentConfigForm";
|
||||
import { RegionalOneClickTab } from "./RegionalOneClickTab";
|
||||
import { RegionalManualTab } from "./RegionalManualTab";
|
||||
|
||||
interface AddRegionalAgentDialogProps {
|
||||
open: boolean;
|
||||
@@ -22,30 +19,54 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
||||
onOpenChange,
|
||||
onAgentAdded
|
||||
}) => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [regionName, setRegionName] = useState("");
|
||||
const [agentIp, setAgentIp] = useState("");
|
||||
const [installCommand, setInstallCommand] = useState<InstallCommand | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { t } = useLanguage();
|
||||
const { toast } = useToast();
|
||||
const [activeTab, setActiveTab] = useState("configure");
|
||||
const [formData, setFormData] = useState({
|
||||
regionName: "",
|
||||
agentIp: "",
|
||||
});
|
||||
const [agentToken, setAgentToken] = useState("");
|
||||
const [agentId, setAgentId] = useState("");
|
||||
const [currentPocketBaseUrl, setCurrentPocketBaseUrl] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleCreateAgent = async (e: React.FormEvent) => {
|
||||
// Generate new credentials when dialog opens or after successful creation
|
||||
const generateNewCredentials = () => {
|
||||
const newToken = `rgn_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
|
||||
const newAgentId = `regional_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
setAgentToken(newToken);
|
||||
setAgentId(newAgentId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const endpoint = getCurrentEndpoint();
|
||||
setCurrentPocketBaseUrl(endpoint);
|
||||
generateNewCredentials();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!regionName.trim() || !agentIp.trim()) return;
|
||||
if (!formData.regionName.trim() || !formData.agentIp.trim()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await regionalService.createRegionalService({
|
||||
region_name: regionName,
|
||||
agent_ip_address: agentIp,
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
setInstallCommand(result.installCommand);
|
||||
setStep(2);
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('agentCreatedSuccessfully'),
|
||||
});
|
||||
setActiveTab("one-click");
|
||||
generateNewCredentials();
|
||||
|
||||
onAgentAdded();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create regional agent configuration.",
|
||||
title: t('error'),
|
||||
description: t('failedToCreateAgent'),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -53,407 +74,65 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string, description: string = "Content") => {
|
||||
try {
|
||||
// Try the modern clipboard API first
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: `${description} copied to clipboard.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for older browsers or non-secure contexts (like localhost)
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (successful) {
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: `${description} copied to clipboard.`,
|
||||
});
|
||||
} else {
|
||||
throw new Error('execCommand failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard:', error);
|
||||
|
||||
// Show the text in a modal or alert as final fallback
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
|
||||
let errorMessage = "Failed to copy to clipboard.";
|
||||
if (isLocalhost) {
|
||||
errorMessage += " This is common in local development. Please manually copy the text.";
|
||||
} else if (userAgent.includes('chrome')) {
|
||||
errorMessage += " Try using HTTPS or enable clipboard permissions.";
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Copy failed",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
// As a last resort, select the text for manual copying
|
||||
try {
|
||||
const textarea = document.querySelector('textarea[readonly]') as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, 99999); // For mobile devices
|
||||
}
|
||||
} catch (selectError) {
|
||||
// console.error('Failed to select text:', selectError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const downloadScript = () => {
|
||||
if (!installCommand) return;
|
||||
|
||||
const blob = new Blob([installCommand.bash_script], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `install-regional-agent-${installCommand.agent_id}.sh`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: "Downloaded!",
|
||||
description: "Installation script downloaded successfully.",
|
||||
const handleDialogClose = () => {
|
||||
setFormData({
|
||||
regionName: "",
|
||||
agentIp: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
onAgentAdded();
|
||||
setStep(1);
|
||||
setRegionName("");
|
||||
setAgentIp("");
|
||||
setInstallCommand(null);
|
||||
setActiveTab("configure");
|
||||
generateNewCredentials();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const resetDialog = () => {
|
||||
setStep(1);
|
||||
setRegionName("");
|
||||
setAgentIp("");
|
||||
setInstallCommand(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Regional Monitoring Agent</DialogTitle>
|
||||
<DialogTitle>{t('addRegionalMonitoringAgent')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Deploy a regional monitoring agent with automatic one-click installation.
|
||||
{t('deployRegionalMonitoringAgent')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleCreateAgent} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="regionName">Region Name</Label>
|
||||
<Input
|
||||
id="regionName"
|
||||
placeholder="e.g., us-east-1, europe-west-1, asia-pacific-1"
|
||||
value={regionName}
|
||||
onChange={(e) => setRegionName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="agentIp">Agent Server IP Address</Label>
|
||||
<Input
|
||||
id="agentIp"
|
||||
placeholder="e.g., 192.168.1.100 or your-server.example.com"
|
||||
value={agentIp}
|
||||
onChange={(e) => setAgentIp(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="configure">{t('configureAgent')}</TabsTrigger>
|
||||
<TabsTrigger value="one-click">{t('oneClickInstallTab')}</TabsTrigger>
|
||||
<TabsTrigger value="manual">{t('manualInstallTab')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Generating..." : "Generate Installation"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
<TabsContent value="configure" className="space-y-6">
|
||||
<RegionalAgentConfigForm
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
agentId={agentId}
|
||||
agentToken={agentToken}
|
||||
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||
isSubmitting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{step === 2 && installCommand && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<CheckCircle className="h-12 w-12 text-green-600 mx-auto" />
|
||||
<h3 className="text-lg font-semibold">Agent Configuration Ready!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
One-click installation script generated with automatic configuration.
|
||||
</p>
|
||||
</div>
|
||||
<TabsContent value="one-click" className="space-y-6">
|
||||
<RegionalOneClickTab
|
||||
agentToken={agentToken}
|
||||
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||
formData={formData}
|
||||
agentId={agentId}
|
||||
onDialogClose={handleDialogClose}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<Tabs defaultValue="oneclicK" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="oneclicK">One-Click Install</TabsTrigger>
|
||||
<TabsTrigger value="details">Agent Details</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual Install</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="oneclicK" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-yellow-500" />
|
||||
One-Click Automatic Installation
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Complete installation, configuration, and service startup in one command
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-green-800 font-medium mb-2">
|
||||
<Play className="h-4 w-4" />
|
||||
What this script does automatically:
|
||||
</div>
|
||||
<ul className="text-sm text-green-700 space-y-1 ml-6">
|
||||
<li>• Downloads the latest .deb package</li>
|
||||
<li>• Installs the regional monitoring agent</li>
|
||||
<li>• Creates configuration file with your settings</li>
|
||||
<li>• Starts and enables the service</li>
|
||||
<li>• Runs health checks</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Run this command on your target server:</Label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
readOnly
|
||||
value={`# One-click installation command:
|
||||
curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
|
||||
--region-name="${regionName}" \\
|
||||
--agent-id="${installCommand.agent_id}" \\
|
||||
--agent-ip="${agentIp}" \\
|
||||
--token="${installCommand.token}" \\
|
||||
--pocketbase-url="${installCommand.api_endpoint}"`}
|
||||
className="font-mono text-sm min-h-[120px] pr-12"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={downloadScript} variant="outline" className="flex-1">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download Complete Script
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(installCommand.bash_script, "Installation script")}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Full Script
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Local development notice */}
|
||||
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Local Development Notice:</strong> If clipboard copying fails, this is normal in local development.
|
||||
You can use the "Download Complete Script" button instead or manually copy the text.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="details" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Generated Agent Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
These values will be automatically configured during installation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-foreground">Agent ID</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={installCommand.agent_id}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||
onClick={() => copyToClipboard(installCommand.agent_id, "Agent ID")}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-foreground">Region Name</Label>
|
||||
<Input
|
||||
value={regionName}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-foreground">Server IP</Label>
|
||||
<Input
|
||||
value={agentIp}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-foreground">API Endpoint</Label>
|
||||
<Input
|
||||
value={installCommand.api_endpoint}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-foreground">Authentication Token</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={installCommand.token}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||
onClick={() => copyToClipboard(installCommand.token, "Authentication token")}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Configuration file location:</strong> /etc/regional-check-agent/regional-check-agent.env
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5" />
|
||||
Manual Installation Steps
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Step-by-step manual installation process
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-sm space-y-4">
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<p className="font-medium">Step 1: Download Package</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1">✅ If you’re using the amd64 architecture, please download this package. </p> <br/>
|
||||
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb
|
||||
</code>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1">✅ If you’re using the arm64 architecture, please download this package. </p> <br/>
|
||||
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<p className="font-medium">Step 2: Install Package</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb<br/>
|
||||
sudo apt-get install -f
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<p className="font-medium">Step 3: Configure Agent</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
sudo nano /etc/regional-check-agent/regional-check-agent.env
|
||||
</code>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
Copy sample configuration from https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf
|
||||
</code>
|
||||
<p className="text-xs text-muted-foreground mt-1">Add the configuration values shown in the Agent Details tab</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<p className="font-medium">Step 4: Start Service</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
sudo systemctl enable regional-check-agent<br/>
|
||||
sudo systemctl start regional-check-agent
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-green-500 pl-4">
|
||||
<p className="font-medium">Step 5: Verify Installation</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
sudo systemctl status regional-check-agent<br/>
|
||||
curl http://localhost:8091/health
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={resetDialog}>
|
||||
Add Another Agent
|
||||
</Button>
|
||||
<Button onClick={handleComplete}>
|
||||
Complete Setup
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TabsContent value="manual" className="space-y-6">
|
||||
<RegionalManualTab
|
||||
agentToken={agentToken}
|
||||
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||
formData={formData}
|
||||
agentId={agentId}
|
||||
onDialogClose={handleDialogClose}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MapPin, Wifi, WifiOff, MoreVertical, Trash2, Terminal, Copy } from "luc
|
||||
import { RegionalService } from "@/types/regional.types";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface RegionalAgentCardProps {
|
||||
agent: RegionalService;
|
||||
@@ -16,6 +17,7 @@ interface RegionalAgentCardProps {
|
||||
|
||||
export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onDelete }) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Check if this is the default agent that cannot be removed
|
||||
const isDefaultAgent = agent.agent_id === "1" || agent.region_name === "Default";
|
||||
@@ -24,13 +26,13 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
try {
|
||||
await navigator.clipboard.writeText(agent.agent_id);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Agent ID copied to clipboard.",
|
||||
title: t('copied'),
|
||||
description: t('copiedDescription'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Copy failed",
|
||||
description: "Failed to copy agent ID.",
|
||||
title: t('copyFailed'),
|
||||
description: t('copyFailedDescription'),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -40,14 +42,14 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
if (agent.connection === 'online') {
|
||||
return {
|
||||
icon: <Wifi className="h-4 w-4" />,
|
||||
label: 'Online',
|
||||
label: t('online'),
|
||||
variant: 'default' as const,
|
||||
className: 'bg-green-100 text-green-800 hover:bg-green-100'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
icon: <WifiOff className="h-4 w-4" />,
|
||||
label: 'Offline',
|
||||
label: t('offline'),
|
||||
variant: 'secondary' as const,
|
||||
className: 'bg-red-100 text-red-800 hover:bg-red-100'
|
||||
};
|
||||
@@ -67,7 +69,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
{agent.region_name}
|
||||
{isDefaultAgent && (
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
|
||||
Default
|
||||
{t('defaultBadge')}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
@@ -86,12 +88,12 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={copyAgentId}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Agent ID
|
||||
{t('copyAgentId')}
|
||||
</DropdownMenuItem>
|
||||
{!isDefaultAgent && (
|
||||
<DropdownMenuItem onClick={onDelete} className="text-red-600">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Remove Agent
|
||||
{t('removeAgent')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
@@ -116,12 +118,12 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Agent ID:</span>
|
||||
<span className="text-muted-foreground">{t('agentId')}</span>
|
||||
<span className="font-mono text-xs">{agent.agent_id.substring(0, 12)}...</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Last Updated:</span>
|
||||
<span className="text-muted-foreground">{t('lastUpdated')}</span>
|
||||
<span className="text-xs">
|
||||
{formatDistanceToNow(new Date(agent.updated), { addSuffix: true })}
|
||||
</span>
|
||||
@@ -132,7 +134,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex items-center text-xs text-green-600">
|
||||
<div className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse"></div>
|
||||
Active monitoring
|
||||
{t('activeMonitoring')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -141,7 +143,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex items-center text-xs text-red-600">
|
||||
<div className="w-2 h-2 bg-red-600 rounded-full mr-2"></div>
|
||||
Connection lost
|
||||
{t('connectionLost')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Copy } from "lucide-react";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface RegionalAgentConfigFormProps {
|
||||
formData: {
|
||||
regionName: string;
|
||||
agentIp: string;
|
||||
};
|
||||
setFormData: (data: { regionName: string; agentIp: string }) => void;
|
||||
agentId: string;
|
||||
agentToken: string;
|
||||
currentPocketBaseUrl: string;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
export const RegionalAgentConfigForm: React.FC<RegionalAgentConfigFormProps> = ({
|
||||
formData,
|
||||
setFormData,
|
||||
agentId,
|
||||
agentToken,
|
||||
currentPocketBaseUrl,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="regionName">{t('regionName')}</Label>
|
||||
<Input
|
||||
id="regionName"
|
||||
placeholder={t('regionNamePlaceholder')}
|
||||
value={formData.regionName}
|
||||
onChange={(e) => setFormData({ ...formData, regionName: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="agentIp">{t('agentServerIpAddress')}</Label>
|
||||
<Input
|
||||
id="agentIp"
|
||||
placeholder={t('agentIpPlaceholder')}
|
||||
value={formData.agentIp}
|
||||
onChange={(e) => setFormData({ ...formData, agentIp: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('agentId')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={agentId}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||
onClick={() => copyToClipboard(agentId)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('apiEndpoint')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={currentPocketBaseUrl}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||
onClick={() => copyToClipboard(currentPocketBaseUrl)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('authenticationToken')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={agentToken}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||
onClick={() => copyToClipboard(agentToken)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? t('creating') : t('createAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Copy, Terminal } from "lucide-react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface RegionalManualTabProps {
|
||||
agentToken: string;
|
||||
currentPocketBaseUrl: string;
|
||||
formData: {
|
||||
regionName: string;
|
||||
agentIp: string;
|
||||
};
|
||||
agentId: string;
|
||||
onDialogClose: () => void;
|
||||
}
|
||||
|
||||
export const RegionalManualTab: React.FC<RegionalManualTabProps> = ({
|
||||
agentToken,
|
||||
currentPocketBaseUrl,
|
||||
formData,
|
||||
agentId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const getManualInstallSteps = () => {
|
||||
return [
|
||||
{
|
||||
title: t('step1DownloadPackage'),
|
||||
commands: [
|
||||
{
|
||||
label: t('downloadAmd64Notice'),
|
||||
command: 'wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb'
|
||||
},
|
||||
{
|
||||
label: t('downloadArm64Notice'),
|
||||
command: 'wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('step2InstallPackage'),
|
||||
commands: [
|
||||
{
|
||||
label: '',
|
||||
command: 'sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb\nsudo apt-get install -f'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('step3ConfigureAgent'),
|
||||
commands: [
|
||||
{
|
||||
label: '',
|
||||
command: 'sudo nano /etc/regional-check-agent/regional-check-agent.env'
|
||||
},
|
||||
{
|
||||
label: t('addConfigurationValues'),
|
||||
command: `POCKETBASE_URL=${currentPocketBaseUrl}
|
||||
AGENT_TOKEN=${agentToken}
|
||||
AGENT_ID=${agentId}
|
||||
REGION_NAME=${formData.regionName}
|
||||
AGENT_IP=${formData.agentIp}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('step4StartService'),
|
||||
commands: [
|
||||
{
|
||||
label: '',
|
||||
command: 'sudo systemctl enable regional-check-agent\nsudo systemctl start regional-check-agent'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('step5VerifyInstallation'),
|
||||
commands: [
|
||||
{
|
||||
label: '',
|
||||
command: 'sudo systemctl status regional-check-agent\ncurl http://localhost:8091/health'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5" />
|
||||
{t('manualInstallationSteps')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('stepByStepManualInstallation')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
{getManualInstallSteps().map((step, stepIndex) => (
|
||||
<div key={stepIndex} className="border-l-4 border-blue-500 pl-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm font-medium">
|
||||
{stepIndex + 1}
|
||||
</span>
|
||||
<p className="font-medium">{step.title}</p>
|
||||
</div>
|
||||
{step.commands.map((cmd, cmdIndex) => (
|
||||
<div key={cmdIndex} className="space-y-1">
|
||||
{cmd.label && (
|
||||
<p className="text-xs text-muted-foreground ml-8">{cmd.label}</p>
|
||||
)}
|
||||
<div className="ml-8 relative">
|
||||
<pre className="bg-muted p-3 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
|
||||
<code>{cmd.command}</code>
|
||||
</pre>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => copyToClipboard(cmd.command)}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">Prerequisites:</h4>
|
||||
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-disc list-inside mb-3">
|
||||
<li>Ensure you have root/sudo access on the target server</li>
|
||||
<li>Make sure wget or curl is installed for downloading files</li>
|
||||
<li>Internet connection required for downloading packages</li>
|
||||
</ul>
|
||||
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
The agent will start automatically and appear in your dashboard within a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -9,8 +9,10 @@ import { RegionalService } from "@/types/regional.types";
|
||||
import { AddRegionalAgentDialog } from "./AddRegionalAgentDialog";
|
||||
import { RegionalAgentCard } from "./RegionalAgentCard";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export const RegionalMonitoringContent = () => {
|
||||
const { t } = useLanguage();
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -24,8 +26,8 @@ export const RegionalMonitoringContent = () => {
|
||||
const handleAgentAdded = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
||||
toast({
|
||||
title: "Regional Agent Added",
|
||||
description: "The regional monitoring agent has been successfully configured.",
|
||||
title: t('regionalAgentAdded'),
|
||||
description: t('regionalAgentAddedDesc'),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,13 +36,13 @@ export const RegionalMonitoringContent = () => {
|
||||
await regionalService.deleteRegionalService(id);
|
||||
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
||||
toast({
|
||||
title: "Agent Removed",
|
||||
description: "The regional monitoring agent has been removed.",
|
||||
title: t('agentRemoved'),
|
||||
description: t('agentRemovedDesc'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to remove the regional monitoring agent.",
|
||||
title: t('error'),
|
||||
description: t('failedToRemoveAgent'),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -53,14 +55,14 @@ export const RegionalMonitoringContent = () => {
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('regionalmonitoring')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage distributed monitoring agents across different regions
|
||||
{t('descriptRegionPage')}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Regional Agent
|
||||
{t('addRegionalAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -68,39 +70,39 @@ export const RegionalMonitoringContent = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{t('totalAgents')}</CardTitle>
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalAgents}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Regional monitoring agents
|
||||
{t('regionalMonitoringAgents')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Online Agents</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{t('onlineAgents')}</CardTitle>
|
||||
<Wifi className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Currently connected
|
||||
{t('currentlyConnected')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Offline Agents</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{t('offlineAgents')}</CardTitle>
|
||||
<WifiOff className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Disconnected agents
|
||||
{t('disconnectedAgents')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -108,7 +110,7 @@ export const RegionalMonitoringContent = () => {
|
||||
|
||||
{/* Agents List */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Regional Agents</h2>
|
||||
<h2 className="text-xl font-semibold">{t('regionalAgents')}</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@@ -131,13 +133,13 @@ export const RegionalMonitoringContent = () => {
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('noRegionalAgents')}</h3>
|
||||
<p className="text-muted-foreground text-center mb-4">
|
||||
Get started by adding your first regional monitoring agent to extend your monitoring coverage.
|
||||
{t('getStartedAddAgent')}
|
||||
</p>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add First Agent
|
||||
{t('addFirstAgent')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Copy, Download, Zap, Play } from "lucide-react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface RegionalOneClickTabProps {
|
||||
agentToken: string;
|
||||
currentPocketBaseUrl: string;
|
||||
formData: {
|
||||
regionName: string;
|
||||
agentIp: string;
|
||||
};
|
||||
agentId: string;
|
||||
onDialogClose: () => void;
|
||||
}
|
||||
|
||||
export const RegionalOneClickTab: React.FC<RegionalOneClickTabProps> = ({
|
||||
agentToken,
|
||||
currentPocketBaseUrl,
|
||||
formData,
|
||||
agentId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { toast } = useToast();
|
||||
|
||||
const getOneClickCommand = () => {
|
||||
return `curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
|
||||
--region-name="${formData.regionName}" \\
|
||||
--agent-id="${agentId}" \\
|
||||
--agent-ip="${formData.agentIp}" \\
|
||||
--token="${agentToken}" \\
|
||||
--pocketbase-url="${currentPocketBaseUrl}"`;
|
||||
};
|
||||
|
||||
const downloadScript = () => {
|
||||
const scriptContent = getOneClickCommand();
|
||||
const blob = new Blob([scriptContent], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `install-regional-agent-${agentId}.sh`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: t('downloaded'),
|
||||
description: t('downloadedDescription'),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-green-500/20 bg-green-50/50 dark:bg-green-950/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
|
||||
<Zap className="h-5 w-5" />
|
||||
{t('oneClickAutomaticInstallation')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-green-600 dark:text-green-300">
|
||||
{t('completeInstallationDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 dark:bg-green-950/50 dark:border-green-800">
|
||||
<div className="flex items-center gap-2 text-green-800 dark:text-green-200 font-medium mb-2">
|
||||
<Play className="h-4 w-4" />
|
||||
{t('whatThisScriptDoes')}
|
||||
</div>
|
||||
<ul className="text-sm text-green-700 dark:text-green-300 space-y-1 ml-6">
|
||||
<li>• {t('scriptActionDownload')}</li>
|
||||
<li>• {t('scriptActionInstall')}</li>
|
||||
<li>• {t('scriptActionConfigure')}</li>
|
||||
<li>• {t('scriptActionStart')}</li>
|
||||
<li>• {t('scriptActionHealthChecks')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-green-700 dark:text-green-400">{t('runCommandOnServer')}</Label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
readOnly
|
||||
value={getOneClickCommand()}
|
||||
className="font-mono text-sm min-h-[120px] pr-12 bg-muted/50 border-green-200 dark:border-green-800"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute top-2 right-2 bg-green-50 dark:bg-green-950/50 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900"
|
||||
onClick={() => copyToClipboard(getOneClickCommand())}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={downloadScript} variant="outline" className="flex-1">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{t('downloadCompleteScript')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 dark:bg-yellow-950/20 dark:border-yellow-800">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200" dangerouslySetInnerHTML={{ __html: t('localDevelopmentNotice') }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
|
||||
import { OneClickInstallTab } from "./OneClickInstallTab";
|
||||
import { DockerOneClickTab } from "./DockerOneClickTab";
|
||||
import { ManualInstallTab } from "./ManualInstallTab";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface AddServerAgentDialogProps {
|
||||
open: boolean;
|
||||
@@ -28,15 +29,13 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
onOpenChange,
|
||||
onAgentAdded,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("configure");
|
||||
|
||||
// Get current PocketBase URL
|
||||
const currentPocketBaseUrl = getCurrentEndpoint();
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
serverName: "",
|
||||
description: "",
|
||||
@@ -47,23 +46,46 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
notificationEnabled: true,
|
||||
});
|
||||
|
||||
// Generated server token and agent ID
|
||||
const [serverToken] = useState(() =>
|
||||
const generateNewCredentials = () => ({
|
||||
serverToken: `srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`,
|
||||
serverId: `agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
|
||||
});
|
||||
|
||||
const [serverToken, setServerToken] = useState(() =>
|
||||
`srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
|
||||
);
|
||||
|
||||
const [serverId] = useState(() =>
|
||||
|
||||
const [serverId, setServerId] = useState(() =>
|
||||
`agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
|
||||
);
|
||||
|
||||
// Refresh credentials and reset form whenever the dialog opens
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
const newCredentials = generateNewCredentials();
|
||||
setServerToken(newCredentials.serverToken);
|
||||
setServerId(newCredentials.serverId);
|
||||
setActiveTab("configure");
|
||||
setFormData({
|
||||
serverName: "",
|
||||
description: "",
|
||||
osType: "",
|
||||
checkInterval: "60",
|
||||
retryAttempt: "3",
|
||||
dockerEnabled: false,
|
||||
notificationEnabled: true,
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isSubmitting) return;
|
||||
|
||||
if (!formData.serverName || !formData.osType) {
|
||||
toast({
|
||||
title: "Validation Error",
|
||||
description: "Please fill in all required fields.",
|
||||
title: t('validationError'),
|
||||
description: t('fillRequiredFields'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -72,22 +94,18 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Here you would typically create the server monitoring configuration
|
||||
// For now, we'll simulate the process
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
toast({
|
||||
title: "Server Agent Created",
|
||||
description: `${formData.serverName} monitoring agent has been configured successfully.`,
|
||||
title: t('serverAgentCreated'),
|
||||
description: t('serverAgentCreatedDesc').replace('{name}', formData.serverName),
|
||||
});
|
||||
|
||||
// Switch to one-click install tab after successful creation
|
||||
setActiveTab("one-click");
|
||||
onAgentAdded();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create server monitoring agent.",
|
||||
title: t('error'),
|
||||
description: t('failedToCreateAgent'),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -96,17 +114,6 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
// Reset form and tab when dialog closes
|
||||
setActiveTab("configure");
|
||||
setFormData({
|
||||
serverName: "",
|
||||
description: "",
|
||||
osType: "",
|
||||
checkInterval: "60",
|
||||
retryAttempt: "3",
|
||||
dockerEnabled: false,
|
||||
notificationEnabled: true,
|
||||
});
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -116,19 +123,19 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5" />
|
||||
Add Server Monitoring Agent
|
||||
{t('addServerMonitoringAgent')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure a new server monitoring agent to track system metrics and performance.
|
||||
{t('configureAgentDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="configure">Configure Agent</TabsTrigger>
|
||||
<TabsTrigger value="one-click">One-Click Install</TabsTrigger>
|
||||
<TabsTrigger value="docker-one-click">Docker One-Click</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual Installation</TabsTrigger>
|
||||
<TabsTrigger value="configure">{t('configureAgent')}</TabsTrigger>
|
||||
<TabsTrigger value="one-click">{t('oneClickInstall')}</TabsTrigger>
|
||||
<TabsTrigger value="docker-one-click">{t('dockerOneClick')}</TabsTrigger>
|
||||
<TabsTrigger value="manual">{t('manualInstallation')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="configure" className="space-y-6">
|
||||
@@ -152,7 +159,7 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
onDialogClose={handleDialogClose}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
<TabsContent value="docker-one-click" className="space-y-6">
|
||||
<DockerOneClickTab
|
||||
serverToken={serverToken}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Copy, Download, Container } from "lucide-react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DockerOneClickTabProps {
|
||||
serverToken: string;
|
||||
@@ -26,6 +27,7 @@ export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
|
||||
serverId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const getDockerOneClickCommand = () => {
|
||||
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
|
||||
|
||||
@@ -86,15 +88,15 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
|
||||
<Container className="h-5 w-5" />
|
||||
Docker One-Click Install
|
||||
{t('dockerOneClickTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-blue-600 dark:text-blue-300">
|
||||
Automated Docker container installation with system monitoring capabilities
|
||||
{t('dockerOneClickDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-blue-700 dark:text-blue-400">Docker One-Click Command</Label>
|
||||
<Label className="text-blue-700 dark:text-blue-400">{t('dockerOneClickCommand')}</Label>
|
||||
<div className="relative">
|
||||
<pre className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-blue-800 dark:text-blue-200">
|
||||
<code>{getDockerOneClickCommand()}</code>
|
||||
@@ -107,18 +109,18 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
onClick={handleCopyOneClickCommand}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 p-3 rounded-md">
|
||||
<p className="font-medium mb-1">This script will automatically:</p>
|
||||
<p className="font-medium mb-1">{t('dockerScriptWill')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Download and setup the Docker monitoring agent</li>
|
||||
<li>Configure all required environment variables</li>
|
||||
<li>Start the container with proper system access</li>
|
||||
<li>Setup monitoring data persistence</li>
|
||||
<li>{t('dockerScriptStep1')}</li>
|
||||
<li>{t('dockerScriptStep2')}</li>
|
||||
<li>{t('dockerScriptStep3')}</li>
|
||||
<li>{t('dockerScriptStep4')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -129,15 +131,15 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Container className="h-5 w-5" />
|
||||
Direct Docker Run Command
|
||||
{t('directDockerTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
If you prefer to run the Docker container directly without the script
|
||||
{t('directDockerDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Docker Run Command</Label>
|
||||
<Label>{t('dockerRunCommand')}</Label>
|
||||
<div className="relative">
|
||||
<pre className="bg-muted border p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
|
||||
<code>{getDirectDockerCommand()}</code>
|
||||
@@ -150,17 +152,17 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
onClick={handleCopyDockerCommand}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground bg-muted/50 border p-3 rounded-md">
|
||||
<p className="font-medium mb-1">Prerequisites for direct Docker run:</p>
|
||||
<p className="font-medium mb-1">{t('dockerPrerequisites')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Docker must be installed and running</li>
|
||||
<li>The operacle/checkcle-server-agent image must be available</li>
|
||||
<li>Run as root or with sudo privileges</li>
|
||||
<li>{t('dockerPrereqStep1')}</li>
|
||||
<li>{t('dockerPrereqStep2')}</li>
|
||||
<li>{t('dockerPrereqStep3')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -168,7 +170,7 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
Done
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RefreshCw, X } from "lucide-react";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { templateService, ServerNotificationTemplate } from "@/services/templateService";
|
||||
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface EditServerDialogProps {
|
||||
server: Server | null;
|
||||
@@ -46,6 +47,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
onOpenChange,
|
||||
onServerUpdated,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [formData, setFormData] = useState<ServerFormData>({
|
||||
name: "",
|
||||
check_interval: 60,
|
||||
@@ -114,7 +116,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Handle the API response format with proper field names and type conversion
|
||||
setThresholdFormData({
|
||||
cpu_threshold: parseInt(String(existingThreshold.cpu_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((existingThreshold as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((existingThreshold.ram_threshold_message ?? existingThreshold.ram_threshold))) || 80,
|
||||
disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80,
|
||||
network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80,
|
||||
});
|
||||
@@ -141,7 +143,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Handle the API response format with proper field names and type conversion
|
||||
setThresholdFormData({
|
||||
cpu_threshold: parseInt(String(threshold.cpu_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((threshold as any).ram_threshold_message || threshold.ram_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((threshold.ram_threshold_message ?? threshold.ram_threshold))) || 80,
|
||||
disk_threshold: parseInt(String(threshold.disk_threshold)) || 80,
|
||||
network_threshold: parseInt(String(threshold.network_threshold)) || 80,
|
||||
});
|
||||
@@ -165,8 +167,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load notification channels",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToLoadChannels', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setLoadingAlertConfigs(false);
|
||||
@@ -182,8 +184,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load templates",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToLoadTemplates', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setLoadingTemplates(false);
|
||||
@@ -198,8 +200,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load server thresholds",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToLoadThresholds', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setLoadingThresholds(false);
|
||||
@@ -274,15 +276,15 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Warning",
|
||||
description: "Server updated but failed to update threshold values.",
|
||||
title: t('warning', 'instance'),
|
||||
description: t('serverUpdatedButThresholdFailed', 'instance'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Server updated",
|
||||
description: `${formData.name} has been updated successfully.`,
|
||||
title: t('serverUpdated', 'instance'),
|
||||
description: t('serverUpdatedDesc', 'instance', { name: formData.name }),
|
||||
});
|
||||
|
||||
onServerUpdated();
|
||||
@@ -291,8 +293,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to update server. Please try again.",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToUpdateServer', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -323,62 +325,62 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Server Configuration</DialogTitle>
|
||||
<DialogTitle>{t('editServerConfiguration', 'instance')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="serverName">Server Name *</Label>
|
||||
<Label htmlFor="serverName">{t('serverNameLabel', 'instance')}</Label>
|
||||
<Input
|
||||
id="serverName"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Enter server name"
|
||||
placeholder={t('serverNamePlaceholder', 'instance')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="checkInterval">Check Interval</Label>
|
||||
<Label htmlFor="checkInterval">{t('checkIntervalLabel', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.check_interval.toString()}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, check_interval: parseInt(value) }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select interval" />
|
||||
<SelectValue placeholder={t('selectInterval', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">30 seconds</SelectItem>
|
||||
<SelectItem value="60">1 minute</SelectItem>
|
||||
<SelectItem value="120">2 minutes</SelectItem>
|
||||
<SelectItem value="300">5 minutes</SelectItem>
|
||||
<SelectItem value="600">10 minutes</SelectItem>
|
||||
<SelectItem value="30">{t('interval30s', 'instance')}</SelectItem>
|
||||
<SelectItem value="60">{t('interval1m', 'instance')}</SelectItem>
|
||||
<SelectItem value="120">{t('interval2m', 'instance')}</SelectItem>
|
||||
<SelectItem value="300">{t('interval5m', 'instance')}</SelectItem>
|
||||
<SelectItem value="600">{t('interval10m', 'instance')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxRetries">Max Retries</Label>
|
||||
<Label htmlFor="maxRetries">{t('maxRetriesLabel', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.max_retries.toString()}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, max_retries: parseInt(value) }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select max retries" />
|
||||
<SelectValue placeholder={t('selectMaxRetries', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 retry</SelectItem>
|
||||
<SelectItem value="2">2 retries</SelectItem>
|
||||
<SelectItem value="3">3 retries</SelectItem>
|
||||
<SelectItem value="5">5 retries</SelectItem>
|
||||
<SelectItem value="10">10 retries</SelectItem>
|
||||
<SelectItem value="1">{t('retry1', 'instance')}</SelectItem>
|
||||
<SelectItem value="2">{t('retry2', 'instance')}</SelectItem>
|
||||
<SelectItem value="3">{t('retry3', 'instance')}</SelectItem>
|
||||
<SelectItem value="5">{t('retry5', 'instance')}</SelectItem>
|
||||
<SelectItem value="10">{t('retry10', 'instance')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dockerMonitoring">Docker Monitoring</Label>
|
||||
<Label htmlFor="dockerMonitoring">{t('dockerMonitoring', 'instance')}</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="dockerMonitoring"
|
||||
@@ -389,7 +391,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="dockerMonitoring" className="text-sm text-muted-foreground">
|
||||
{formData.docker_monitoring ? "Enabled" : "Disabled"}
|
||||
{formData.docker_monitoring ? t('enabled', 'instance') : t('disabled', 'instance')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -407,22 +409,22 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Remove the automatic clearing of notification_channels, threshold_id, and template_id
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
|
||||
<Label htmlFor="notificationEnabled">{t('enableNotifications', 'instance')}</Label>
|
||||
</div>
|
||||
|
||||
{/* Expanded Notification Settings */}
|
||||
{formData.notification_enabled && (
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Notification Settings</CardTitle>
|
||||
<CardTitle className="text-lg">{t('notificationSettings', 'instance')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Multiple Notification Channels Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Notification Channels</Label>
|
||||
<Label>{t('notificationChannels', 'instance')}</Label>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto border rounded-md p-3">
|
||||
{loadingAlertConfigs ? (
|
||||
<div className="text-sm text-muted-foreground">Loading channels...</div>
|
||||
<div className="text-sm text-muted-foreground">{t('loadingChannels', 'instance')}</div>
|
||||
) : alertConfigs.length > 0 ? (
|
||||
alertConfigs.map((config) => (
|
||||
<div key={config.id} className="flex items-center space-x-2">
|
||||
@@ -442,14 +444,14 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">No notification channels available</div>
|
||||
<div className="text-sm text-muted-foreground">{t('noChannelsAvailable', 'instance')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Channels Display */}
|
||||
{formData.notification_channels.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Selected Channels:</Label>
|
||||
<Label className="text-sm font-medium">{t('selectedChannels', 'instance')}</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.notification_channels.map((channelId) => {
|
||||
const channel = alertConfigs.find(c => c.id === channelId);
|
||||
@@ -478,17 +480,17 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
|
||||
{/* Server Set Threshold Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thresholdId">Server Set Threshold</Label>
|
||||
<Label htmlFor="thresholdId">{t('serverSetThreshold', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.threshold_id}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, threshold_id: value }))}
|
||||
disabled={loadingThresholds}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingThresholds ? "Loading thresholds..." : "Select server threshold"} />
|
||||
<SelectValue placeholder={loadingThresholds ? t('loadingThresholds', 'instance') : t('selectServerThreshold', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No threshold (use default)</SelectItem>
|
||||
<SelectItem value="none">{t('noThreshold', 'instance')}</SelectItem>
|
||||
{thresholds.map((threshold) => (
|
||||
<SelectItem key={threshold.id} value={threshold.id}>
|
||||
{threshold.name}
|
||||
@@ -502,12 +504,12 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
{selectedThreshold && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
|
||||
<CardTitle className="text-base">{t('thresholdDetails', 'instance')}: {selectedThreshold.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('cpuThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -521,7 +523,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('ramThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -535,7 +537,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('diskThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -549,7 +551,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Network Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('networkThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -569,17 +571,17 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
|
||||
{/* Server Template Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="templateId">Server Template</Label>
|
||||
<Label htmlFor="templateId">{t('serverTemplate', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.template_id}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, template_id: value }))}
|
||||
disabled={loadingTemplates}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingTemplates ? "Loading templates..." : "Select server template"} />
|
||||
<SelectValue placeholder={loadingTemplates ? t('loadingTemplates', 'instance') : t('selectServerTemplate', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No template (use default)</SelectItem>
|
||||
<SelectItem value="none">{t('noTemplate', 'instance')}</SelectItem>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
@@ -593,44 +595,44 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
{selectedTemplate && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Template Details: {selectedTemplate.name}</CardTitle>
|
||||
<CardTitle className="text-base">{t('templateDetails', 'instance')}: {selectedTemplate.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 text-sm">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">RAM Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('ramMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.ram_message || "No RAM message defined"}
|
||||
{selectedTemplate.ram_message || t('noMessageDefined', 'instance', { name: 'RAM' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">CPU Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('cpuMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.cpu_message || "No CPU message defined"}
|
||||
{selectedTemplate.cpu_message || t('noMessageDefined', 'instance', { name: 'CPU' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Disk Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('diskMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.disk_message || "No disk message defined"}
|
||||
{selectedTemplate.disk_message || t('noMessageDefined', 'instance', { name: 'disk' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Network Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('networkMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.network_message || "No network message defined"}
|
||||
{selectedTemplate.network_message || t('noMessageDefined', 'instance', { name: 'network' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Up Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('upMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.up_message || "No up message defined"}
|
||||
{selectedTemplate.up_message || t('noMessageDefined', 'instance', { name: 'up' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Down Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('downMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.down_message || "No down message defined"}
|
||||
{selectedTemplate.down_message || t('noMessageDefined', 'instance', { name: 'down' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -649,16 +651,16 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel', 'instance')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Updating...
|
||||
{t('updating', 'instance')}
|
||||
</>
|
||||
) : (
|
||||
"Update Server"
|
||||
t('updateServer', 'instance')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Copy, Terminal } from "lucide-react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ManualInstallTabProps {
|
||||
serverToken: string;
|
||||
@@ -24,20 +25,22 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
serverId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const getManualInstallSteps = () => {
|
||||
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
|
||||
|
||||
return [
|
||||
{
|
||||
title: "Download the installation script",
|
||||
title: t('downloadScript'),
|
||||
command: `curl -L -o server-agent.sh "${scriptUrl}"`
|
||||
},
|
||||
{
|
||||
title: "Make the script executable",
|
||||
title: t('makeExecutable'),
|
||||
command: `chmod +x server-agent.sh`
|
||||
},
|
||||
{
|
||||
title: "Run the installation with your configuration",
|
||||
title: t('runInstall'),
|
||||
command: `SERVER_TOKEN="${serverToken}" POCKETBASE_URL="${currentPocketBaseUrl}" SERVER_NAME="${formData.serverName}" AGENT_ID="${serverId}" sudo bash server-agent.sh`
|
||||
}
|
||||
];
|
||||
@@ -48,26 +51,26 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5" />
|
||||
Manual Installation Steps
|
||||
{t('manualInstallTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Step-by-step installation process
|
||||
{t('manualInstallDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">Server Name:</span> {formData.serverName}
|
||||
<span className="font-medium">{t('serverName')}:</span> {formData.serverName}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Agent ID:</span> {serverId}
|
||||
<span className="font-medium">{t('agentId')}:</span> {serverId}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">OS Type:</span> {formData.osType}
|
||||
<span className="font-medium">{t('osType')}:</span> {formData.osType}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Check Interval:</span> {formData.checkInterval}s
|
||||
<span className="font-medium">{t('checkInterval')}:</span> {formData.checkInterval}s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +96,7 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
onClick={() => copyToClipboard(step.command)}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,22 +104,22 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">Prerequisites:</h4>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">{t('prerequisites')}</h4>
|
||||
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-disc list-inside mb-3">
|
||||
<li>Ensure you have root/sudo access on the target server</li>
|
||||
<li>Make sure curl is installed for downloading files</li>
|
||||
<li>Internet connection required for downloading script</li>
|
||||
<li>{t('prereqRoot')}</li>
|
||||
<li>{t('prereqCurl')}</li>
|
||||
<li>{t('prereqInternet')}</li>
|
||||
</ul>
|
||||
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">{t('afterInstall')}</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
The agent will start automatically and appear in your dashboard within a few minutes.
|
||||
{t('agentWillStart')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
Done
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Copy, Download } from "lucide-react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface OneClickInstallTabProps {
|
||||
serverToken: string;
|
||||
@@ -26,6 +27,8 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
|
||||
serverId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const getOneClickInstallCommand = () => {
|
||||
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
|
||||
|
||||
@@ -55,15 +58,15 @@ sudo -E bash ./server-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
|
||||
<Download className="h-5 w-5" />
|
||||
One-Click Install (Recommended)
|
||||
{t('oneClickInstallTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-green-600 dark:text-green-300">
|
||||
Copy and paste this single command to install the monitoring agent instantly
|
||||
{t('oneClickInstallDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-green-700 dark:text-green-400">Quick Install Command</Label>
|
||||
<Label className="text-green-700 dark:text-green-400">{t('quickInstallCommand')}</Label>
|
||||
<div className="relative">
|
||||
<pre className="bg-black-50 dark:bg-green-100/950 border border-green-200 dark:border-green-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-green-800 dark:text-green-200">
|
||||
<code>{getOneClickInstallCommand()}</code>
|
||||
@@ -76,23 +79,23 @@ sudo -E bash ./server-agent.sh`;
|
||||
onClick={handleCopyCommand}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 p-3 rounded-md">
|
||||
<p className="font-medium mb-1">Simply run this command on your server:</p>
|
||||
<p className="font-medium mb-1">{t('runCommandOnServer')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>SSH into your target server</li>
|
||||
<li>Paste and run the command above</li>
|
||||
<li>The agent will be installed and started automatically</li>
|
||||
<li>{t('sshIntoServer')}</li>
|
||||
<li>{t('pasteAndRun')}</li>
|
||||
<li>{t('agentInstalled')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
Done
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Copy } from "lucide-react";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
import { OSSelector } from "./OSSelector";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServerAgentConfigFormProps {
|
||||
formData: {
|
||||
@@ -43,23 +44,25 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="serverName">Server Name *</Label>
|
||||
<Label htmlFor="serverName">{t('serverName')} *</Label>
|
||||
<Input
|
||||
id="serverName"
|
||||
placeholder="e.g., web-server-01"
|
||||
placeholder={t('serverNamePlaceholder')}
|
||||
value={formData.serverName}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, serverName: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">What is the name or label used as the identifier</p>
|
||||
<p className="text-xs text-muted-foreground">{t('serverNameDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="serverId">Server Agent ID</Label>
|
||||
<Label htmlFor="serverId">{t('serverAgentId')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="serverId"
|
||||
@@ -76,13 +79,13 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Auto-generated unique identifier</p>
|
||||
<p className="text-xs text-muted-foreground">{t('serverAgentIdDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Operating System *</Label>
|
||||
<Label>{t('operatingSystem')} *</Label>
|
||||
<OSSelector
|
||||
value={formData.osType}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, osType: value }))}
|
||||
@@ -91,45 +94,45 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="checkInterval">Check Interval</Label>
|
||||
<Label htmlFor="checkInterval">{t('checkInterval')}</Label>
|
||||
<Select
|
||||
value={formData.checkInterval}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, checkInterval: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select interval" />
|
||||
<SelectValue placeholder={t('selectInterval')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">30 seconds</SelectItem>
|
||||
<SelectItem value="60">1 minute</SelectItem>
|
||||
<SelectItem value="120">2 minutes</SelectItem>
|
||||
<SelectItem value="300">5 minutes</SelectItem>
|
||||
<SelectItem value="30">{t('interval30s')}</SelectItem>
|
||||
<SelectItem value="60">{t('interval1m')}</SelectItem>
|
||||
<SelectItem value="120">{t('interval2m')}</SelectItem>
|
||||
<SelectItem value="300">{t('interval5m')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">How often to check the server and metric status</p>
|
||||
<p className="text-xs text-muted-foreground">{t('checkIntervalDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="retryAttempt">Retry Attempts</Label>
|
||||
<Label htmlFor="retryAttempt">{t('retryAttempts')}</Label>
|
||||
<Select
|
||||
value={formData.retryAttempt}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, retryAttempt: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select retry attempts" />
|
||||
<SelectValue placeholder={t('selectRetryAttempts')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 attempt</SelectItem>
|
||||
<SelectItem value="2">2 attempts</SelectItem>
|
||||
<SelectItem value="3">3 attempts</SelectItem>
|
||||
<SelectItem value="5">5 attempts</SelectItem>
|
||||
<SelectItem value="1">{t('attempt1')}</SelectItem>
|
||||
<SelectItem value="2">{t('attempt2')}</SelectItem>
|
||||
<SelectItem value="3">{t('attempt3')}</SelectItem>
|
||||
<SelectItem value="5">{t('attempt5')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Number of retry attempts before marking as down</p>
|
||||
<p className="text-xs text-muted-foreground">{t('retryAttemptsDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Server Token</Label>
|
||||
<Label>{t('serverToken')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={serverToken} readOnly className="font-mono text-sm bg-muted" />
|
||||
<Button
|
||||
@@ -141,11 +144,11 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Auto-generated authentication token</p>
|
||||
<p className="text-xs text-muted-foreground">{t('serverTokenDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>System URL</Label>
|
||||
<Label>{t('systemUrl')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={currentPocketBaseUrl} readOnly className="font-mono text-sm bg-muted" />
|
||||
<Button
|
||||
@@ -157,14 +160,14 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Current system API URL</p>
|
||||
<p className="text-xs text-muted-foreground">{t('systemUrlDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button type="submit" disabled={isSubmitting} className="w-full">
|
||||
{isSubmitting ? "Creating Agent..." : "Create Server Agent"}
|
||||
{isSubmitting ? t('creatingAgent') : t('createServerAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -8,12 +8,14 @@ import { CPUChart } from "./charts/CPUChart";
|
||||
import { MemoryChart } from "./charts/MemoryChart";
|
||||
import { DiskChart } from "./charts/DiskChart";
|
||||
import { NetworkChart } from "./charts/NetworkChart";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServerHistoryChartsProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
const { t } = useLanguage();
|
||||
const {
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
@@ -33,15 +35,15 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
|
||||
// Show skeleton loading state for better UX
|
||||
if (isLoading) {
|
||||
return (
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<h2 className="text-lg font-medium">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-xs text-muted-foreground">Loading...</span>
|
||||
<span className="text-xs text-muted-foreground">{t("loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
@@ -77,7 +79,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<h2 className="text-lg font-medium">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
|
||||
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
@@ -85,9 +87,9 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">Error loading chart data</p>
|
||||
<p className="text-muted-foreground">{t("errorLoadingChartData")}</p>
|
||||
<p className="text-xs mt-2 font-mono text-red-500">{error?.message}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">Server ID: {serverId} • Time Range: {timeRange}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">{t("serverIdTimeRange", { serverId, timeRange })}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -101,7 +103,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<h2 className="text-lg font-medium">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
|
||||
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
@@ -109,11 +111,13 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">No historical data available for {timeRange}</p>
|
||||
<p className="text-xs mt-2">Raw metrics count: {metrics.length}</p>
|
||||
<p className="text-xs mt-1">Server ID: {serverId} • Time Range: {timeRange}</p>
|
||||
<p className="text-muted-foreground">{t("noHistoricalData", { timeRange })}</p>
|
||||
<p className="text-xs mt-2">{t("rawMetricsCount", { count: metrics.length })}</p>
|
||||
<p className="text-xs mt-1">{t("serverIdTimeRange", { serverId, timeRange })}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">
|
||||
{metrics.length > 0 ? 'Data exists but may be outside selected time range' : 'No metrics data found'}
|
||||
{metrics.length > 0
|
||||
? t("dataExistsOutsideRange")
|
||||
: t("noMetricsDataFound")}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -129,13 +133,13 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<h2 className="text-lg font-medium">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({chartData.length} data points • {timeRange})
|
||||
{isFetching && (
|
||||
<span className="inline-flex items-center gap-1 ml-2">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span className="text-blue-500">Updating...</span>
|
||||
<span className="text-blue-500">{t("updating")}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -3,17 +3,19 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Server, Activity, AlertTriangle, Power } from "lucide-react";
|
||||
import { ServerStats } from "@/types/server.types";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServerStatsCardsProps {
|
||||
stats: ServerStats;
|
||||
}
|
||||
|
||||
export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
const { t } = useLanguage();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "TOTAL SERVERS",
|
||||
title: t('totalServers', 'instance'),
|
||||
value: stats.total,
|
||||
icon: Server,
|
||||
gradient: theme === 'dark'
|
||||
@@ -21,7 +23,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #a0522d 100%)"
|
||||
},
|
||||
{
|
||||
title: "ONLINE SERVERS",
|
||||
title: t('onlineServers', 'instance'),
|
||||
value: stats.online,
|
||||
icon: Activity,
|
||||
gradient: theme === 'dark'
|
||||
@@ -29,7 +31,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #66bb6a 100%)"
|
||||
},
|
||||
{
|
||||
title: "OFFLINE SERVERS",
|
||||
title: t('offlineServers', 'instance'),
|
||||
value: stats.offline,
|
||||
icon: Power,
|
||||
gradient: theme === 'dark'
|
||||
@@ -37,7 +39,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #ef5350 100%)"
|
||||
},
|
||||
{
|
||||
title: "WARNING SERVERS",
|
||||
title: t('warningServers', 'instance'),
|
||||
value: stats.warning,
|
||||
icon: AlertTriangle,
|
||||
gradient: theme === 'dark'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
@@ -16,6 +17,7 @@ import { serverService } from "@/services/serverService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServerTableProps {
|
||||
servers: Server[];
|
||||
@@ -24,6 +26,7 @@ interface ServerTableProps {
|
||||
}
|
||||
|
||||
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
|
||||
const { t } = useLanguage();
|
||||
const { theme } = useTheme();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -31,6 +34,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [pausingServers, setPausingServers] = useState<Set<string>>(new Set());
|
||||
const [selectedServerIds, setSelectedServerIds] = useState<Set<string>>(new Set());
|
||||
const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false);
|
||||
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -40,6 +46,25 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const allVisibleSelected = filteredServers.length > 0 && filteredServers.every(s => selectedServerIds.has(s.id));
|
||||
const someVisibleSelected = filteredServers.some(s => selectedServerIds.has(s.id)) && !allVisibleSelected;
|
||||
|
||||
const toggleSelectAllVisible = (checked: boolean) => {
|
||||
const newSet = new Set(selectedServerIds);
|
||||
if (checked) {
|
||||
filteredServers.forEach(s => newSet.add(s.id));
|
||||
} else {
|
||||
filteredServers.forEach(s => newSet.delete(s.id));
|
||||
}
|
||||
setSelectedServerIds(newSet);
|
||||
};
|
||||
|
||||
const toggleSelectOne = (serverId: string, checked: boolean) => {
|
||||
const newSet = new Set(selectedServerIds);
|
||||
if (checked) newSet.add(serverId); else newSet.delete(serverId);
|
||||
setSelectedServerIds(newSet);
|
||||
};
|
||||
|
||||
const handleViewDetails = (serverId: string) => {
|
||||
navigate(`/server-detail/${serverId}`);
|
||||
};
|
||||
@@ -68,8 +93,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
await pb.collection('servers').update(serverId, updateData);
|
||||
|
||||
toast({
|
||||
title: isPaused ? "Server resumed" : "Server paused",
|
||||
description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
|
||||
title: isPaused ? t('serverResumed') : t('serverPaused'),
|
||||
description: isPaused ? t('monitoringResumed', { name: server.name }) : t('monitoringPaused', { name: server.name }),
|
||||
});
|
||||
|
||||
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
|
||||
@@ -81,8 +106,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
// console.error('Error updating server status:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
|
||||
title: t('error'),
|
||||
description: isPaused ? t('resumeServerError') : t('pauseServerError'),
|
||||
});
|
||||
} finally {
|
||||
setPausingServers(prev => {
|
||||
@@ -136,6 +161,33 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBulkDelete = async () => {
|
||||
if (selectedServerIds.size === 0 || isBulkDeleting) return;
|
||||
try {
|
||||
setIsBulkDeleting(true);
|
||||
const ids = Array.from(selectedServerIds);
|
||||
const deletions = ids.map(id => pb.collection('servers').delete(id));
|
||||
const results = await Promise.allSettled(deletions);
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
if (failed === 0) {
|
||||
toast({ title: "Servers deleted", description: `${ids.length} server(s) have been deleted.` });
|
||||
} else if (failed === ids.length) {
|
||||
toast({ variant: "destructive", title: "Error", description: "Failed to delete selected servers. Please try again." });
|
||||
} else {
|
||||
toast({ variant: "destructive", title: "Partial success", description: `Deleted ${ids.length - failed}, failed ${failed}.` });
|
||||
}
|
||||
|
||||
onRefresh();
|
||||
setSelectedServerIds(new Set());
|
||||
setBulkDeleteDialogOpen(false);
|
||||
} catch (_e) {
|
||||
toast({ variant: "destructive", title: "Error", description: "Failed to delete selected servers. Please try again." });
|
||||
} finally {
|
||||
setIsBulkDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const CustomProgressBar = ({
|
||||
value,
|
||||
label,
|
||||
@@ -204,12 +256,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
return (
|
||||
<Card className="flex-1 flex flex-col">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<CardTitle>Servers</CardTitle>
|
||||
<CardTitle>{t('servers')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex items-center justify-center">
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<RefreshCw className="h-6 w-6 animate-spin" />
|
||||
<span className="ml-2">Loading servers...</span>
|
||||
<span className="ml-2">{t('loadingServers')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -221,17 +273,29 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<Card className="bg-transparent border-0 shadow-none">
|
||||
<CardHeader className="pb-4 px-0">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<CardTitle className="text-xl font-semibold">Servers</CardTitle>
|
||||
<CardTitle className="text-xl font-semibold">{t('servers')}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1 sm:w-64">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search servers..."
|
||||
placeholder={t('searchServersPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
{selectedServerIds.size > 0 && (
|
||||
<div className="hidden sm:block text-sm text-muted-foreground mr-2">
|
||||
{selectedServerIds.size} selected
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setBulkDeleteDialogOpen(true)}
|
||||
variant="destructive"
|
||||
disabled={selectedServerIds.size === 0}
|
||||
>
|
||||
Delete Selected
|
||||
</Button>
|
||||
<Button onClick={onRefresh} variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -241,23 +305,33 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<CardContent className="p-0">
|
||||
{filteredServers.length === 0 ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<p className="text-muted-foreground">No servers found</p>
|
||||
<p className="text-muted-foreground">{t('noServersFound')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
|
||||
<Table>
|
||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
||||
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Status</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>OS</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>IP Address</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Last Checked</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>Actions</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} w-10`}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={allVisibleSelected}
|
||||
onCheckedChange={(v) => toggleSelectAllVisible(Boolean(v))}
|
||||
aria-label="Select all"
|
||||
indeterminate={someVisibleSelected}
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('name')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('OS')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('IPAddress')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('CPU')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('memory')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('disk')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('uptime')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('lastChecked')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -268,12 +342,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
const isPaused = server.status === "paused";
|
||||
const isProcessing = pausingServers.has(server.id);
|
||||
|
||||
const isSelected = selectedServerIds.has(server.id);
|
||||
return (
|
||||
<TableRow
|
||||
key={server.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
className={`hover:bg-muted/50 cursor-pointer ${isSelected ? 'bg-muted/30' : ''}`}
|
||||
onClick={() => handleViewDetails(server.id)}
|
||||
>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(v) => toggleSelectOne(server.id, Boolean(v))}
|
||||
aria-label={`Select ${server.name}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div className="truncate" title={server.name}>
|
||||
{server.name}
|
||||
@@ -333,7 +415,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<span className="sr-only">{t('openMenu')}</span>
|
||||
{isProcessing ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
@@ -344,12 +426,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Server Detail
|
||||
{t('viewServerDetail')}
|
||||
</DropdownMenuItem>
|
||||
{server.docker === 'true' && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Container Monitoring
|
||||
{t('containerMonitoring')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
@@ -360,26 +442,26 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
{isPaused ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Resume Monitoring
|
||||
{t('resumeMonitoring')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
Pause Monitoring
|
||||
{t('pauseMonitoring')}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Server
|
||||
{t('editServer')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Server
|
||||
{t('deleteServer')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -406,25 +488,45 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteServerConfirmTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete{' '}
|
||||
<span className="font-semibold text-foreground">
|
||||
{selectedServer?.name}
|
||||
</span>{' '}
|
||||
and all of its monitoring data.
|
||||
{t('deleteServerConfirmDesc').replace('{name}', selectedServer?.name ?? '')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 text-white hover:bg-red-700"
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
{isDeleting ? t('deleting') : t('delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Bulk Delete Confirmation Dialog */}
|
||||
<AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete selected servers?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete {selectedServerIds.size} server(s) and all of their monitoring data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isBulkDeleting}>
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmBulkDelete}
|
||||
disabled={isBulkDeleting}
|
||||
className="bg-red-600 text-white hover:bg-red-700"
|
||||
>
|
||||
{isBulkDeleting ? t('deleting') : 'Delete Selected'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
+3
-10
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||
import { Header } from "@/components/dashboard/Header";
|
||||
@@ -7,11 +6,9 @@ import { ServiceNotFound } from "@/components/services/ServiceNotFound";
|
||||
import { Service } from "@/types/service.types";
|
||||
|
||||
interface ServiceDetailWrapperProps {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
service: Service | null;
|
||||
sidebarCollapsed: boolean;
|
||||
toggleSidebar: () => void;
|
||||
currentUser: any;
|
||||
handleLogout: () => void;
|
||||
}
|
||||
@@ -20,20 +17,16 @@ export const ServiceDetailWrapper = ({
|
||||
children,
|
||||
isLoading,
|
||||
service,
|
||||
sidebarCollapsed,
|
||||
toggleSidebar,
|
||||
currentUser,
|
||||
handleLogout
|
||||
}: ServiceDetailWrapperProps) => {
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -48,4 +41,4 @@ export const ServiceDetailWrapper = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,78 +1,34 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { DateRangeOption } from "../DateRangeFilter";
|
||||
import { authService } from "@/services/authService";
|
||||
import { ServiceDetailContent } from "../ServiceDetailContent";
|
||||
import { ServiceDetailWrapper } from "./ServiceDetailWrapper";
|
||||
import { useServiceData, useRealTimeUpdates } from "./hooks";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
|
||||
export const ServiceDetailContainer = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Set default to 24h
|
||||
const [startDate, setStartDate] = useState<Date>(() => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - 24); // Go back 24 hours
|
||||
date.setHours(date.getHours() - 24);
|
||||
return date;
|
||||
});
|
||||
const [endDate, setEndDate] = useState<Date>(() => {
|
||||
const date = new Date();
|
||||
date.setMinutes(date.getMinutes() + 5); // Add 5 minutes buffer to future
|
||||
date.setMinutes(date.getMinutes() + 5);
|
||||
return date;
|
||||
});
|
||||
const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h');
|
||||
|
||||
// State for sidebar collapse functionality (shared with Dashboard)
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||
// Check if there's a saved preference in localStorage
|
||||
const saved = localStorage.getItem("sidebarCollapsed");
|
||||
return saved ? JSON.parse(saved) : window.innerWidth < 768;
|
||||
});
|
||||
|
||||
// Toggle sidebar and save preference
|
||||
const toggleSidebar = useCallback(() => {
|
||||
setSidebarCollapsed(prev => {
|
||||
const newState = !prev;
|
||||
localStorage.setItem("sidebarCollapsed", JSON.stringify(newState));
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Get current user for header
|
||||
const currentUser = authService.getCurrentUser();
|
||||
|
||||
useEffect(() => {
|
||||
// Verify user is authenticated
|
||||
if (!authService.isAuthenticated()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Authentication required",
|
||||
description: "Please log in to view service details",
|
||||
});
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
// Auto-collapse sidebar on small screens
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth < 768 && !sidebarCollapsed) {
|
||||
setSidebarCollapsed(true);
|
||||
localStorage.setItem("sidebarCollapsed", JSON.stringify(true));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [navigate, sidebarCollapsed]);
|
||||
|
||||
// Handler for logout (same as Dashboard)
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
// Use our custom hooks
|
||||
const {
|
||||
service,
|
||||
uptimeData,
|
||||
@@ -85,7 +41,6 @@ export const ServiceDetailContainer = () => {
|
||||
handleRegionalAgentChange
|
||||
} = useServiceData(id, startDate, endDate);
|
||||
|
||||
// Set up real-time updates
|
||||
useRealTimeUpdates({
|
||||
serviceId: id,
|
||||
startDate,
|
||||
@@ -94,18 +49,12 @@ export const ServiceDetailContainer = () => {
|
||||
setUptimeData
|
||||
});
|
||||
|
||||
// Handle date range filter changes
|
||||
const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => {
|
||||
console.log(`ServiceDetailContainer: Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
|
||||
|
||||
// Update state which will trigger the useEffect in useServiceData
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
setSelectedRange(option);
|
||||
|
||||
// Also explicitly fetch data with the new range to ensure immediate update
|
||||
if (id) {
|
||||
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
|
||||
fetchUptimeData(id, start, end, option, selectedRegionalAgent);
|
||||
}
|
||||
}, [id, fetchUptimeData, selectedRegionalAgent]);
|
||||
@@ -114,12 +63,10 @@ export const ServiceDetailContainer = () => {
|
||||
<ServiceDetailWrapper
|
||||
isLoading={isLoading}
|
||||
service={service}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
currentUser={currentUser}
|
||||
handleLogout={handleLogout}
|
||||
>
|
||||
{service && (
|
||||
{service ? (
|
||||
<ServiceDetailContent
|
||||
service={service}
|
||||
uptimeData={uptimeData}
|
||||
@@ -129,7 +76,7 @@ export const ServiceDetailContainer = () => {
|
||||
selectedRegionalAgent={selectedRegionalAgent}
|
||||
onRegionalAgentChange={handleRegionalAgentChange}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</ServiceDetailWrapper>
|
||||
);
|
||||
};
|
||||
@@ -63,9 +63,9 @@ export function ServicesPagination({
|
||||
return (
|
||||
<div className="flex items-center justify-between py-4 px-4 border-t border-border">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("rowsPerPage") ?? "Rows per page"}:
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{t("rowsPerPage") ?? "Rows per page"}:
|
||||
</span>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={(value) => onPageSizeChange(parseInt(value) as PageSize)}
|
||||
|
||||
@@ -25,6 +25,10 @@ export const useServiceSchema = () => {
|
||||
type: z.enum(["http", "ping", "tcp", "dns"]),
|
||||
url: z.string()
|
||||
.min(1, t("urlDomainHostRequired"))
|
||||
.refine(
|
||||
(value) => !/\s/.test(value),
|
||||
t("spacesNotAllowed")
|
||||
)
|
||||
.refine(
|
||||
(value) => value.trim().length > 0,
|
||||
t("enterValidUrlHostnameDomain")
|
||||
|
||||
@@ -158,14 +158,14 @@ const DataRetentionSettings = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
{t("dataRetention", "settings")}
|
||||
{t("dataRetention")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||
<AlertTriangle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<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 data retention settings. These settings can only be accessed and modified by Super Admins.
|
||||
<span className="font-medium">{t("permissionNotice")}</span> {t("permissionNoticeDataRetention")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
@@ -178,7 +178,7 @@ const DataRetentionSettings = () => {
|
||||
return (
|
||||
<div className="p-4 flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
Loading retention settings...
|
||||
{t("loadingRetentionSettings")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -189,16 +189,16 @@ const DataRetentionSettings = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Data Retention Settings
|
||||
{t("dataRetention")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure how long monitoring data is kept in the system
|
||||
{t("dataRetentionDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="uptimeRetention">Uptime Monitoring Retention (days)</Label>
|
||||
<Label htmlFor="uptimeRetention">{t("uptimeRetentionLabel")}</Label>
|
||||
<Input
|
||||
id="uptimeRetention"
|
||||
type="number"
|
||||
@@ -218,12 +218,12 @@ const DataRetentionSettings = () => {
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Service uptime and incident data older than this will be automatically deleted
|
||||
{t("uptimeRetentionHelp")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="serverRetention">Server Monitoring Retention (days)</Label>
|
||||
<Label htmlFor="serverRetention">{t("serverRetentionLabel")}</Label>
|
||||
<Input
|
||||
id="serverRetention"
|
||||
type="number"
|
||||
@@ -243,7 +243,7 @@ const DataRetentionSettings = () => {
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Server metrics and process data older than this will be automatically deleted
|
||||
{t("serverRetentionHelp")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@ const DataRetentionSettings = () => {
|
||||
<Alert>
|
||||
<Database className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Last automatic cleanup: {new Date(lastCleanup).toLocaleString()}
|
||||
{t("lastCleanup")}: {new Date(lastCleanup).toLocaleString()}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -266,7 +266,7 @@ const DataRetentionSettings = () => {
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Save Changes
|
||||
{t("save")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
+159
-92
@@ -38,7 +38,7 @@ interface NotificationChannelDialogProps {
|
||||
|
||||
const baseSchema = z.object({
|
||||
notify_name: z.string().min(1, "Name is required"),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "notifiarr", "gotify", "webhook"]),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "notifiarr", "gotify", "webhook", "matrix"]),
|
||||
enabled: z.boolean().default(true),
|
||||
service_id: z.string().default("global"),
|
||||
template_id: z.string().optional(),
|
||||
@@ -110,6 +110,13 @@ const gotifySchema = baseSchema.extend({
|
||||
server_url: z.string().url("Must be a valid server URL"),
|
||||
});
|
||||
|
||||
const matrixSchema = baseSchema.extend({
|
||||
notification_type: z.literal("matrix"),
|
||||
matrix_homeserver: z.string().url("Must be a valid homeserver URL"),
|
||||
matrix_room_id: z.string().min(1, "Room ID is required"),
|
||||
matrix_access_token: z.string().min(1, "Access token is required"),
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("notification_type", [
|
||||
telegramSchema,
|
||||
discordSchema,
|
||||
@@ -122,6 +129,7 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
||||
notifiarrSchema,
|
||||
gotifySchema,
|
||||
webhookSchema,
|
||||
matrixSchema,
|
||||
]);
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@@ -188,12 +196,18 @@ const notificationTypeOptions = [
|
||||
description: "Send push notifications via Gotify",
|
||||
icon: "/upload/notification/gotify.png"
|
||||
},
|
||||
{
|
||||
value: "webhook",
|
||||
label: "Webhook",
|
||||
{
|
||||
value: "webhook",
|
||||
label: "Webhook",
|
||||
description: "Send notifications to custom webhook",
|
||||
icon: "/upload/notification/webhook.png"
|
||||
},
|
||||
{
|
||||
value: "matrix",
|
||||
label: "Matrix",
|
||||
description: "Send notifications to a Matrix chat room",
|
||||
icon: "/upload/notification/matrix.png"
|
||||
},
|
||||
];
|
||||
|
||||
const webhookPayloadTemplates = {
|
||||
@@ -247,6 +261,7 @@ export const NotificationChannelDialog = ({
|
||||
onClose,
|
||||
editingConfig
|
||||
}: NotificationChannelDialogProps) => {
|
||||
const { t } = useLanguage();
|
||||
const isEditing = !!editingConfig;
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -325,7 +340,7 @@ export const NotificationChannelDialog = ({
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing ? "Edit Notification Channel" : "Add Notification Channel"}
|
||||
{isEditing ? t("editChannel") : t("addChannelDialog")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -336,28 +351,28 @@ export const NotificationChannelDialog = ({
|
||||
name="notify_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel Name</FormLabel>
|
||||
<FormLabel>{t("channelName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My Notification Channel" {...field} />
|
||||
<Input placeholder={t("channelNamePlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
A name to identify this notification channel
|
||||
{t("channelNameDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notification_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel Type</FormLabel>
|
||||
<FormLabel>{t("channelType")}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select notification type" />
|
||||
<SelectValue placeholder={t("selectType")} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -370,8 +385,8 @@ export const NotificationChannelDialog = ({
|
||||
className="w-5 h-5 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{option.description}</span>
|
||||
<span className="font-medium">{t(option.value)}</span>
|
||||
<span className="text-xs text-muted-foreground">{t(option.description)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -382,7 +397,7 @@ export const NotificationChannelDialog = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
{notificationType === "telegram" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -390,12 +405,12 @@ export const NotificationChannelDialog = ({
|
||||
name="telegram_chat_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chat ID</FormLabel>
|
||||
<FormLabel>{t("telegramChatId")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Telegram Chat ID" {...field} />
|
||||
<Input placeholder={t("telegramChatIdPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The Telegram chat ID to send notifications to
|
||||
{t("telegramChatIdDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -406,12 +421,12 @@ export const NotificationChannelDialog = ({
|
||||
name="bot_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Token</FormLabel>
|
||||
<FormLabel>{t("botToken")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Telegram Bot Token" {...field} type="password" />
|
||||
<Input placeholder={t("botTokenPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Telegram bot token from @BotFather
|
||||
{t("botTokenDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -419,38 +434,38 @@ export const NotificationChannelDialog = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{notificationType === "discord" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="discord_webhook_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormLabel>{t("discordWebhookUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://discord.com/api/webhooks/..." {...field} />
|
||||
<Input placeholder={t("discordWebhookUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Discord webhook URL from your server settings
|
||||
{t("discordWebhookUrlDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{notificationType === "slack" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slack_webhook_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormLabel>{t("slackWebhookUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://hooks.slack.com/services/..." {...field} />
|
||||
<Input placeholder={t("slackWebhookUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Slack incoming webhook URL
|
||||
{t("slackWebhookUrlDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -465,12 +480,12 @@ export const NotificationChannelDialog = ({
|
||||
name="signal_number"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Signal Number</FormLabel>
|
||||
<FormLabel>{t("signalNumber")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="+1234567890" {...field} />
|
||||
<Input placeholder={t("signalNumberPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Signal phone number to send notifications to
|
||||
{t("signalNumberDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -481,12 +496,12 @@ export const NotificationChannelDialog = ({
|
||||
name="signal_api_endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Signal API Endpoint</FormLabel>
|
||||
<FormLabel>{t("signalApiEndpoint")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://your-signal-api.com/v2/send" {...field} />
|
||||
<Input placeholder={t("signalApiEndpointPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The Rest API endpoint for your Signal service
|
||||
{t("signalApiEndpointDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -501,12 +516,12 @@ export const NotificationChannelDialog = ({
|
||||
name="google_chat_webhook_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Google Chat Webhook URL</FormLabel>
|
||||
<FormLabel>{t("googleChatWebhookUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
|
||||
<Input placeholder={t("googleChatWebhookUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Google Chat webhook URL from your Google Chat space
|
||||
{t("googleChatWebhookUrlDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -521,12 +536,12 @@ export const NotificationChannelDialog = ({
|
||||
name="email_address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormLabel>{t("emailAddress")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="notifications@example.com" {...field} type="email" />
|
||||
<Input placeholder={t("emailAddressPlaceholder")} {...field} type="email" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Email address to send notifications to
|
||||
{t("emailAddressDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -537,12 +552,12 @@ export const NotificationChannelDialog = ({
|
||||
name="email_sender_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sender Name</FormLabel>
|
||||
<FormLabel>{t("emailSenderName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Alert System" {...field} />
|
||||
<Input placeholder={t("emailSenderNamePlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Display name for outgoing emails
|
||||
{t("emailSenderNameDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -554,9 +569,9 @@ export const NotificationChannelDialog = ({
|
||||
name="smtp_server"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Server</FormLabel>
|
||||
<FormLabel>{t("smtpServer")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="smtp.gmail.com" {...field} />
|
||||
<Input placeholder={t("smtpServerPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -567,9 +582,9 @@ export const NotificationChannelDialog = ({
|
||||
name="smtp_port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Port</FormLabel>
|
||||
<FormLabel>{t("smtpPort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="587" {...field} />
|
||||
<Input placeholder={t("smtpPortPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -581,12 +596,12 @@ export const NotificationChannelDialog = ({
|
||||
name="smtp_password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Password</FormLabel>
|
||||
<FormLabel>{t("smtpPassword")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter your SMTP password" {...field} type="password" />
|
||||
<Input placeholder={t("smtpPasswordPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Password for authenticating with the SMTP server
|
||||
{t("smtpPasswordDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -602,12 +617,12 @@ export const NotificationChannelDialog = ({
|
||||
name="ntfy_endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>NTFY Endpoint</FormLabel>
|
||||
<FormLabel>{t("ntfyEndpoint")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
|
||||
<Input placeholder={t("ntfyEndpointPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
|
||||
{t("ntfyEndpointDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -618,12 +633,12 @@ export const NotificationChannelDialog = ({
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token (Optional)</FormLabel>
|
||||
<FormLabel>{t("apiTokenOptional")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter NTFY API token" {...field} type="password" />
|
||||
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional API token for authentication with NTFY server
|
||||
{t("apiTokenDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -632,19 +647,19 @@ export const NotificationChannelDialog = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "pushover" && (
|
||||
{notificationType === "pushover" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormLabel>{t("apiToken")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Pushover API token" {...field} type="password" />
|
||||
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Pushover application API token
|
||||
{t("apiTokenDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -655,12 +670,12 @@ export const NotificationChannelDialog = ({
|
||||
name="user_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>User Key</FormLabel>
|
||||
<FormLabel>{t("pushoverUserKey")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Pushover user key" {...field} />
|
||||
<Input placeholder={t("pushoverUserKeyPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Pushover user key (or group key)
|
||||
{t("pushoverUserKeyDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -676,12 +691,12 @@ export const NotificationChannelDialog = ({
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormLabel>{t("apiToken")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Notifiarr API token" {...field} type="password" />
|
||||
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Notifiarr API token for sending notifications
|
||||
{t("apiTokenDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -692,12 +707,12 @@ export const NotificationChannelDialog = ({
|
||||
name="channel_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel ID</FormLabel>
|
||||
<FormLabel>{t("notifiarrChannelId")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Discord Channel ID" {...field} />
|
||||
<Input placeholder={t("notifiarrChannelIdPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The Discord channel ID where notifications will be sent
|
||||
{t("notifiarrChannelIdDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -713,12 +728,12 @@ export const NotificationChannelDialog = ({
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormLabel>{t("apiToken")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Gotify API token" {...field} type="password" />
|
||||
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Gotify application API token
|
||||
{t("apiTokenDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -729,12 +744,12 @@ export const NotificationChannelDialog = ({
|
||||
name="server_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormLabel>{t("gotifyServerUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://your-gotify-server.com" {...field} />
|
||||
<Input placeholder={t("gotifyServerUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The URL of your Gotify server
|
||||
{t("gotifyServerUrlDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -742,7 +757,7 @@ export const NotificationChannelDialog = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{notificationType === "webhook" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -750,12 +765,12 @@ export const NotificationChannelDialog = ({
|
||||
name="webhook_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormLabel>{t("webhookUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://api.example.com/webhook" {...field} />
|
||||
<Input placeholder={t("webhookUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The URL where webhook notifications will be sent
|
||||
{t("webhookUrlDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -768,10 +783,9 @@ export const NotificationChannelDialog = ({
|
||||
name="webhook_payload_template"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Payload Template (Optional)</FormLabel>
|
||||
|
||||
<FormLabel>{t("payloadTemplate")}</FormLabel>
|
||||
<FormDescription>
|
||||
JSON template for the webhook payload. Leave empty to use default template.
|
||||
{t("payloadTemplateDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -780,15 +794,15 @@ export const NotificationChannelDialog = ({
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Payload Templates</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{t("payloadTemplates")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium mb-2">Available Placeholders:</h4>
|
||||
<h4 className="text-sm font-medium mb-2">{t("availablePlaceholders")}</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-muted-foreground">Server:</p>
|
||||
<p className="font-medium text-muted-foreground">{t("server")}</p>
|
||||
<div className="space-y-0.5">
|
||||
<code className="bg-muted px-1 rounded">${'{server_name}'}</code><br/>
|
||||
<code className="bg-muted px-1 rounded">${'{cpu_usage}'}</code><br/>
|
||||
@@ -797,7 +811,7 @@ export const NotificationChannelDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-muted-foreground">Service:</p>
|
||||
<p className="font-medium text-muted-foreground">{t("service")}</p>
|
||||
<div className="space-y-0.5">
|
||||
<code className="bg-muted px-1 rounded">${'{service_name}'}</code><br/>
|
||||
<code className="bg-muted px-1 rounded">${'{response_time}'}</code><br/>
|
||||
@@ -806,7 +820,7 @@ export const NotificationChannelDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-muted-foreground">SSL:</p>
|
||||
<p className="font-medium text-muted-foreground">{t("ssl")}</p>
|
||||
<div className="space-y-0.5">
|
||||
<code className="bg-muted px-1 rounded">${'{domain}'}</code><br/>
|
||||
<code className="bg-muted px-1 rounded">${'{expiry_date}'}</code><br/>
|
||||
@@ -815,7 +829,7 @@ export const NotificationChannelDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-muted-foreground">Common:</p>
|
||||
<p className="font-medium text-muted-foreground">{t("common")}</p>
|
||||
<div className="space-y-0.5">
|
||||
<code className="bg-muted px-1 rounded">${'{status}'}</code><br/>
|
||||
<code className="bg-muted px-1 rounded">${'{time}'}</code><br/>
|
||||
@@ -832,15 +846,68 @@ export const NotificationChannelDialog = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "matrix" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="matrix_homeserver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("matrixHomeserver")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("matrixHomeserverPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("matrixHomeserverDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="matrix_room_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("matrixRoomId")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("matrixRoomIdPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("matrixRoomIdDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="matrix_access_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("matrixAccessToken")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("matrixAccessTokenPlaceholder")} {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("matrixAccessTokenDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Enabled</FormLabel>
|
||||
<FormLabel>{t("enabled")}</FormLabel>
|
||||
<FormDescription>
|
||||
Enable or disable this notification channel
|
||||
{t("enabledDesc")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -855,11 +922,11 @@ export const NotificationChannelDialog = ({
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" type="button" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isEditing ? "Update Channel" : "Create Channel"}
|
||||
{isEditing ? t("updateChannel") : t("createChannel")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
+38
-21
@@ -1,6 +1,7 @@
|
||||
|
||||
import { AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { Bell, Edit, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -33,29 +34,40 @@ export const NotificationChannelList = ({
|
||||
onEdit,
|
||||
onDelete
|
||||
}: NotificationChannelListProps) => {
|
||||
const [optimisticStates, setOptimisticStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleEnabled = async (config: CombinedChannel) => {
|
||||
if (!config.id) return;
|
||||
|
||||
if (config.isWebhook) {
|
||||
// Handle webhook toggle
|
||||
try {
|
||||
const newEnabled = config.enabled ? "off" : "on";
|
||||
const currentEnabled = typeof config.enabled === 'string'
|
||||
? config.enabled === "true" || config.enabled === "on"
|
||||
: !!config.enabled;
|
||||
|
||||
// Apply optimistic update immediately
|
||||
setOptimisticStates(prev => ({
|
||||
...prev,
|
||||
[config.id!]: !currentEnabled
|
||||
}));
|
||||
|
||||
try {
|
||||
if (config.isWebhook) {
|
||||
// Handle webhook toggle
|
||||
const newEnabled = currentEnabled ? "off" : "on";
|
||||
await pb.collection('webhook').update(config.id, {
|
||||
enabled: newEnabled
|
||||
});
|
||||
// Trigger refresh by calling onEdit with empty config
|
||||
onEdit({} as AlertConfiguration);
|
||||
} catch (error) {
|
||||
console.error("Error updating webhook:", error);
|
||||
} else {
|
||||
// Handle alert config toggle
|
||||
await alertConfigService.updateAlertConfiguration(config.id, {
|
||||
enabled: !currentEnabled
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Handle alert config toggle
|
||||
await alertConfigService.updateAlertConfiguration(config.id, {
|
||||
enabled: !config.enabled
|
||||
});
|
||||
|
||||
// The parent component will refresh the list
|
||||
onEdit(config as AlertConfiguration);
|
||||
|
||||
} catch (error) {
|
||||
setOptimisticStates(prev => ({
|
||||
...prev,
|
||||
[config.id!]: currentEnabled
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,6 +82,7 @@ export const NotificationChannelList = ({
|
||||
case "pushover": return "Pushover";
|
||||
case "notifiarr": return "Notifiarr";
|
||||
case "webhook": return "Webhook";
|
||||
case "matrix": return "Matrix";
|
||||
default: return type || "Unknown";
|
||||
}
|
||||
};
|
||||
@@ -90,6 +103,8 @@ export const NotificationChannelList = ({
|
||||
return config.signal_number || '';
|
||||
case "email":
|
||||
return config.email_address || '';
|
||||
case "matrix":
|
||||
return config.matrix_room_id || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -130,12 +145,14 @@ export const NotificationChannelList = ({
|
||||
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
|
||||
{getChannelDetails(channel)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={
|
||||
typeof channel.enabled === 'string'
|
||||
? channel.enabled === "true" || channel.enabled === "on"
|
||||
: !!channel.enabled
|
||||
channel.id && optimisticStates.hasOwnProperty(channel.id)
|
||||
? optimisticStates[channel.id]
|
||||
: typeof channel.enabled === 'string'
|
||||
? channel.enabled === "true" || channel.enabled === "on"
|
||||
: !!channel.enabled
|
||||
}
|
||||
onCheckedChange={() => toggleEnabled(channel)}
|
||||
/>
|
||||
@@ -149,7 +166,7 @@ export const NotificationChannelList = ({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEdit(channel as AlertConfiguration)}
|
||||
disabled={channel.isWebhook} // Disable edit for webhooks for now
|
||||
disabled={channel.isWebhook}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
+14
-11
@@ -9,6 +9,7 @@ import { WebhookConfiguration, webhookService } from "@/services/webhookService"
|
||||
import { NotificationChannelDialog } from "./NotificationChannelDialog";
|
||||
import { NotificationChannelList } from "./NotificationChannelList";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface CombinedChannel extends Partial<AlertConfiguration> {
|
||||
isWebhook?: boolean;
|
||||
@@ -18,6 +19,7 @@ interface CombinedChannel extends Partial<AlertConfiguration> {
|
||||
}
|
||||
|
||||
const NotificationSettings = () => {
|
||||
const { t } = useLanguage();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
const [webhookConfigs, setWebhookConfigs] = useState<WebhookConfiguration[]>([]);
|
||||
@@ -126,13 +128,13 @@ const NotificationSettings = () => {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Notification Settings</CardTitle>
|
||||
<CardTitle>{t("titleNotification")}</CardTitle>
|
||||
<CardDescription>
|
||||
Configure notification channels for your services
|
||||
{t("descriptionChannelsServices")}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button onClick={handleAddNew}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Channel
|
||||
<Plus className="mr-2 h-4 w-4" /> {t("addChannel")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -144,14 +146,15 @@ const NotificationSettings = () => {
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="all">All Channels</TabsTrigger>
|
||||
<TabsTrigger value="telegram">Telegram</TabsTrigger>
|
||||
<TabsTrigger value="discord">Discord</TabsTrigger>
|
||||
<TabsTrigger value="slack">Slack</TabsTrigger>
|
||||
<TabsTrigger value="signal">Signal</TabsTrigger>
|
||||
<TabsTrigger value="google_chat">Google Chat</TabsTrigger>
|
||||
<TabsTrigger value="email">Email</TabsTrigger>
|
||||
<TabsTrigger value="webhook">Webhook</TabsTrigger>
|
||||
<TabsTrigger value="all">{t("all")}</TabsTrigger>
|
||||
<TabsTrigger value="telegram">{t("telegram")}</TabsTrigger>
|
||||
<TabsTrigger value="discord">{t("discord")}</TabsTrigger>
|
||||
<TabsTrigger value="slack">{t("slack")}</TabsTrigger>
|
||||
<TabsTrigger value="signal">{t("signal")}</TabsTrigger>
|
||||
<TabsTrigger value="google_chat">{t("googleChat")}</TabsTrigger>
|
||||
<TabsTrigger value="email">{t("email")}</TabsTrigger>
|
||||
<TabsTrigger value="webhook">{t("webhook")}</TabsTrigger>
|
||||
<TabsTrigger value="matrix">{t("matrix")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={currentTab} className="mt-0">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -17,8 +16,9 @@ import { Loader2, AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import UserTextField from "./form-fields/UserTextField";
|
||||
import UserToggleField from "./form-fields/UserToggleField";
|
||||
import UserProfilePictureField from "./form-fields/UserProfilePictureField";
|
||||
import UserRoleField from "./form-fields/UserRoleField";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface EditUserDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -26,6 +26,7 @@ interface EditUserDialogProps {
|
||||
form: UseFormReturn<any>;
|
||||
user: User | null;
|
||||
onSubmit: (data: any) => void;
|
||||
onImpersonate: (data: any) => void;
|
||||
isSubmitting?: boolean;
|
||||
error?: string | null;
|
||||
}
|
||||
@@ -36,10 +37,18 @@ const EditUserDialog = ({
|
||||
form,
|
||||
user,
|
||||
onSubmit,
|
||||
onImpersonate,
|
||||
isSubmitting = false,
|
||||
error = null
|
||||
}: EditUserDialogProps) => {
|
||||
if (!user) return null;
|
||||
|
||||
const [impersonationDurationSeconds, setImpersonationDurationSeconds] = React.useState<number>(3600);
|
||||
|
||||
const handleImpersonateClick = () => {
|
||||
// Impersonation should not depend on edit form validation.
|
||||
onImpersonate({ user, durationSeconds: impersonationDurationSeconds });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
@@ -62,7 +71,6 @@ const EditUserDialog = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<UserProfilePictureField control={form.control} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<UserTextField
|
||||
@@ -102,6 +110,23 @@ const EditUserDialog = ({
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/* Impersonation settings are not part of the form to avoid validation */}
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label htmlFor="impersonation-duration">Impersonation token duration (seconds)</Label>
|
||||
<Input
|
||||
id="impersonation-duration"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={60}
|
||||
step={60}
|
||||
value={impersonationDurationSeconds}
|
||||
onChange={(e) => setImpersonationDurationSeconds(Number(e.target.value || 0))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default is 3600 (1 hour). Minimum 60 seconds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -127,6 +152,20 @@ const EditUserDialog = ({
|
||||
"Update User"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleImpersonateClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Impersonating...
|
||||
</>
|
||||
) : (
|
||||
"Impersonate"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { copyToClipboard } from "@/utils/copyUtils";
|
||||
|
||||
interface ImpersonationTokenDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
token: string | null;
|
||||
impersonatedUserLabel?: string;
|
||||
}
|
||||
|
||||
const ImpersonationTokenDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
token,
|
||||
impersonatedUserLabel,
|
||||
}: ImpersonationTokenDialogProps) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[650px] w-[95vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Impersonation token</DialogTitle>
|
||||
<DialogDescription>
|
||||
{impersonatedUserLabel
|
||||
? `Use this token to impersonate ${impersonatedUserLabel}. Keep it secret.`
|
||||
: "Use this token to impersonate the selected user. Keep it secret."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="rounded-md border bg-muted p-3">
|
||||
<pre className="text-sm overflow-x-auto whitespace-pre-wrap break-all">
|
||||
<code>{token ?? ""}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (token) copyToClipboard(token);
|
||||
}}
|
||||
disabled={!token}
|
||||
>
|
||||
Copy token
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImpersonationTokenDialog;
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import React from "react";
|
||||
import UserTable from "./UserTable";
|
||||
import AddUserDialog from "./AddUserDialog";
|
||||
import EditUserDialog from "./EditUserDialog";
|
||||
import DeleteUserDialog from "./DeleteUserDialog";
|
||||
import ImpersonationTokenDialog from "./ImpersonationTokenDialog";
|
||||
import { useUserManagement } from "./hooks";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { UserCog, Loader2, AlertCircle, Info, Users, ShieldAlert } from "lucide-react";
|
||||
@@ -34,8 +34,12 @@ const UserManagement = () => {
|
||||
handleDeletePrompt,
|
||||
handleDeleteUser,
|
||||
onSubmit,
|
||||
onImpersonate,
|
||||
onAddUser,
|
||||
fetchUsers
|
||||
fetchUsers,
|
||||
impersonationToken,
|
||||
isImpersonationTokenDialogOpen,
|
||||
setIsImpersonationTokenDialogOpen,
|
||||
} = useUserManagement();
|
||||
|
||||
// Get the current logged in user to check their role
|
||||
@@ -79,7 +83,14 @@ const UserManagement = () => {
|
||||
|
||||
<AddUserDialog isOpen={isAddUserDialogOpen && isSuperAdmin} setIsOpen={setIsAddUserDialogOpen} form={newUserForm} onSubmit={onAddUser} isSubmitting={isSubmitting} />
|
||||
|
||||
<EditUserDialog isOpen={isDialogOpen} setIsOpen={setIsDialogOpen} form={form} user={currentUser} onSubmit={onSubmit} isSubmitting={isSubmitting} error={updateError} />
|
||||
<EditUserDialog isOpen={isDialogOpen} setIsOpen={setIsDialogOpen} form={form} user={currentUser} onImpersonate={onImpersonate} onSubmit={onSubmit} isSubmitting={isSubmitting} error={updateError} />
|
||||
|
||||
<ImpersonationTokenDialog
|
||||
open={isImpersonationTokenDialogOpen}
|
||||
onOpenChange={setIsImpersonationTokenDialogOpen}
|
||||
token={impersonationToken}
|
||||
impersonatedUserLabel={currentUser?.full_name || currentUser?.username}
|
||||
/>
|
||||
|
||||
<DeleteUserDialog isOpen={isDeleting} setIsOpen={setIsDeleting} user={userToDelete} onDelete={handleDeleteUser} isDeleting={isSubmitting} />
|
||||
</AccordionContent>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { UserProfilePictureField } from "./";
|
||||
import { UserTextField } from "./";
|
||||
import { UserToggleField } from "./";
|
||||
import { UserRoleField } from "./";
|
||||
@@ -20,7 +19,6 @@ const AddUserForm = ({ form, onSubmit, isSubmitting }: AddUserFormProps) => {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<UserProfilePictureField control={form.control} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<UserTextField
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useUserForm = () => {
|
||||
passwordConfirm: "",
|
||||
isActive: true,
|
||||
role: "user",
|
||||
avatar: avatarOptions[0].url,
|
||||
avatar: avatarOptions[Math.floor(Math.random() * avatarOptions.length)].url,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import { useUsersList } from "./useUsersList";
|
||||
import { useUserForm } from "./useUserForm";
|
||||
import { useUserDialogs } from "./useUserDialogs";
|
||||
import { useUserOperations } from "./useUserOperations";
|
||||
import { User } from "@/services/userService";
|
||||
import { UserFormValues, NewUserFormValues } from "../userForms";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useUserManagement = () => {
|
||||
const { users, loading, error, fetchUsers } = useUsersList();
|
||||
@@ -32,6 +32,7 @@ export const useUserManagement = () => {
|
||||
handleDeleteUser: baseHandleDeleteUser,
|
||||
onSubmit: baseOnSubmit,
|
||||
onAddUser: baseOnAddUser,
|
||||
onImpersonate: baseOnImpersonate,
|
||||
} = useUserOperations(
|
||||
fetchUsers,
|
||||
setIsDialogOpen,
|
||||
@@ -42,6 +43,9 @@ export const useUserManagement = () => {
|
||||
newUserForm.reset
|
||||
);
|
||||
|
||||
const [impersonationToken, setImpersonationToken] = useState<string | null>(null);
|
||||
const [isImpersonationTokenDialogOpen, setIsImpersonationTokenDialogOpen] = useState(false);
|
||||
|
||||
// Wrapper functions to provide the needed arguments
|
||||
const handleEditUser = (user: User) => {
|
||||
baseHandleEditUser(user, form);
|
||||
@@ -56,6 +60,15 @@ export const useUserManagement = () => {
|
||||
baseOnSubmit(data, currentUser);
|
||||
};
|
||||
|
||||
const onImpersonate = async (data: UserFormValues) => {
|
||||
const token = await baseOnImpersonate(data, currentUser);
|
||||
|
||||
if (token) {
|
||||
setImpersonationToken(token);
|
||||
setIsImpersonationTokenDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddUser = (data: NewUserFormValues) => {
|
||||
baseOnAddUser(data);
|
||||
};
|
||||
@@ -81,6 +94,10 @@ export const useUserManagement = () => {
|
||||
handleDeletePrompt,
|
||||
handleDeleteUser,
|
||||
onSubmit,
|
||||
onImpersonate,
|
||||
onAddUser,
|
||||
impersonationToken,
|
||||
isImpersonationTokenDialogOpen,
|
||||
setIsImpersonationTokenDialogOpen,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
|
||||
@@ -121,6 +120,38 @@ export const useUserOperations = (
|
||||
}
|
||||
};
|
||||
|
||||
const onImpersonate = async (
|
||||
data: UserFormValues & { durationSeconds?: number },
|
||||
currentUser: User | null
|
||||
): Promise<string | undefined> => {
|
||||
const loggedInUser = authService.getCurrentUser();
|
||||
if (!currentUser) return;
|
||||
|
||||
try {
|
||||
const token = await authService.impersonateUser(currentUser.id, data?.durationSeconds ?? 3600);
|
||||
|
||||
toast({
|
||||
title: "Impersonation token generated",
|
||||
description: `Token generated for ${currentUser.full_name || currentUser.username}.`,
|
||||
});
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
let description = "Error impersonating user.";
|
||||
if (loggedInUser?.role !== "superadmin") {
|
||||
description = "Only superadmin users can impersonate other users.";
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Error impersonating user",
|
||||
description,
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const onAddUser = async (data: NewUserFormValues) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
@@ -177,6 +208,7 @@ export const useUserOperations = (
|
||||
return {
|
||||
handleDeleteUser,
|
||||
onSubmit,
|
||||
onImpersonate,
|
||||
onAddUser,
|
||||
};
|
||||
};
|
||||
@@ -17,27 +17,33 @@ import { alertConfigService, AlertConfiguration } from "@/services/alertConfigSe
|
||||
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
const formSchema = z.object({
|
||||
domain: z.string().min(1, "Domain is required"),
|
||||
warning_threshold: z.coerce.number().int().min(1).max(365),
|
||||
expiry_threshold: z.coerce.number().int().min(1).max(30),
|
||||
notification_channels: z.array(z.string()).optional(),
|
||||
alert_template: z.string().optional(),
|
||||
check_interval: z.coerce.number().int().min(1).max(30).optional()
|
||||
});
|
||||
|
||||
interface AddSSLCertificateFormProps {
|
||||
onSubmit: (data: AddSSLCertificateDto) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
export const AddSSLCertificateForm = ({
|
||||
onSubmit,
|
||||
export const AddSSLCertificateForm = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isPending = false
|
||||
isPending = false
|
||||
}: AddSSLCertificateFormProps) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const formSchema = z.object({
|
||||
domain: z.string()
|
||||
.min(1, t("domain") + " is required")
|
||||
.refine(
|
||||
(value) => !/\s/.test(value),
|
||||
t("spacesNotAllowed")
|
||||
),
|
||||
warning_threshold: z.coerce.number().int().min(1).max(365),
|
||||
expiry_threshold: z.coerce.number().int().min(1).max(30),
|
||||
notification_channels: z.array(z.string()).optional(),
|
||||
alert_template: z.string().optional(),
|
||||
check_interval: z.coerce.number().int().min(1).max(30).optional()
|
||||
});
|
||||
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -78,7 +84,7 @@ export const AddSSLCertificateForm = ({
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchData();
|
||||
}, [t]);
|
||||
|
||||
@@ -89,16 +95,16 @@ export const AddSSLCertificateForm = ({
|
||||
domain: values.domain,
|
||||
warning_threshold: values.warning_threshold,
|
||||
expiry_threshold: values.expiry_threshold,
|
||||
notification_channel: values.notification_channels && values.notification_channels.length > 0
|
||||
? values.notification_channels.join(',')
|
||||
notification_channel: values.notification_channels && values.notification_channels.length > 0
|
||||
? values.notification_channels.join(',')
|
||||
: '',
|
||||
notification_id: values.notification_channels && values.notification_channels.length > 0
|
||||
? values.notification_channels.join(',')
|
||||
notification_id: values.notification_channels && values.notification_channels.length > 0
|
||||
? values.notification_channels.join(',')
|
||||
: '',
|
||||
template_id: values.alert_template && values.alert_template !== 'none' ? values.alert_template : '',
|
||||
check_interval: values.check_interval
|
||||
};
|
||||
|
||||
|
||||
await onSubmit(certData);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
@@ -136,7 +142,7 @@ export const AddSSLCertificateForm = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -154,7 +160,6 @@ export const AddSSLCertificateForm = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expiry_threshold"
|
||||
@@ -189,7 +194,6 @@ export const AddSSLCertificateForm = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notification_channels"
|
||||
@@ -197,7 +201,7 @@ export const AddSSLCertificateForm = ({
|
||||
<FormItem>
|
||||
<FormLabel>{t('notificationChannel')} (Multi-select)</FormLabel>
|
||||
<div className="space-y-3">
|
||||
<Select
|
||||
<Select
|
||||
onValueChange={handleAddNotificationChannel}
|
||||
value=""
|
||||
>
|
||||
@@ -247,7 +251,7 @@ export const AddSSLCertificateForm = ({
|
||||
)}
|
||||
</div>
|
||||
<FormDescription className="flex items-center gap-1">
|
||||
<Bell className="h-4 w-4" />
|
||||
<Bell className="h-4 w-4" />
|
||||
{t('whereToSend')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -289,7 +293,6 @@ export const AddSSLCertificateForm = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>{t('cancel')}</Button>
|
||||
<Button type="submit" disabled={isPending || isLoading}>
|
||||
|
||||
@@ -1,25 +1,64 @@
|
||||
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
interface SidebarContextType {
|
||||
sidebarCollapsed: boolean;
|
||||
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||
toggleSidebar: () => void;
|
||||
isMobileOpen: boolean;
|
||||
setIsMobileOpen: (open: boolean) => void;
|
||||
toggleMobileMenu: () => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||
|
||||
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
export const SidebarProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
// Initialize sidebar collapsed state from localStorage with window check for safety
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem("sidebarCollapsed");
|
||||
return saved ? JSON.parse(saved) : false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Fix: added isMobileOpen state for responsive mobile menu
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSidebarCollapsed(prev => !prev);
|
||||
setSidebarCollapsed(prev => {
|
||||
const newState = !prev;
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem("sidebarCollapsed", JSON.stringify(newState));
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
// Fix: added toggleMobileMenu function to manage mobile drawer state
|
||||
const toggleMobileMenu = () => {
|
||||
setIsMobileOpen(prev => !prev);
|
||||
};
|
||||
|
||||
// Fix: added resize listener to close mobile menu when switching to desktop view
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
setIsMobileOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
sidebarCollapsed,
|
||||
setSidebarCollapsed,
|
||||
toggleSidebar
|
||||
toggleSidebar,
|
||||
isMobileOpen,
|
||||
setIsMobileOpen,
|
||||
toggleMobileMenu
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -35,4 +74,4 @@ export const useSidebar = () => {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import PocketBase, { BaseAuthStore } from 'pocketbase';
|
||||
|
||||
// Dynamically detect API base URL from current host (for use in browser)
|
||||
const dynamicBaseUrl =
|
||||
@@ -28,8 +28,9 @@ export const setApiEndpoint = (endpoint: string): void => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the PocketBase client with the current API URL
|
||||
export const pb = new PocketBase(getCurrentEndpoint());
|
||||
// Initialize the PocketBase client with an in-memory auth store so auth tokens
|
||||
// are not persisted in localStorage where XSS can trivially exfiltrate them.
|
||||
export const pb = new PocketBase(getCurrentEndpoint(), new BaseAuthStore());
|
||||
|
||||
// Helper to check if user is authenticated
|
||||
export const isAuthenticated = () => {
|
||||
@@ -39,28 +40,7 @@ export const isAuthenticated = () => {
|
||||
// Export the auth store for use in components
|
||||
export const authStore = pb.authStore;
|
||||
|
||||
// Configure PocketBase to persist authentication between page reloads
|
||||
// Remove any auth tokens that may have been persisted by earlier versions.
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedAuthData = localStorage.getItem('pocketbase_auth');
|
||||
if (storedAuthData) {
|
||||
try {
|
||||
const parsedData = JSON.parse(storedAuthData);
|
||||
pb.authStore.save(parsedData.token, parsedData.model);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stored auth data:', error);
|
||||
localStorage.removeItem('pocketbase_auth');
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to authStore changes to persist authentication
|
||||
pb.authStore.onChange(() => {
|
||||
if (pb.authStore.isValid) {
|
||||
localStorage.setItem('pocketbase_auth', JSON.stringify({
|
||||
token: pb.authStore.token,
|
||||
model: pb.authStore.model
|
||||
}));
|
||||
} else {
|
||||
localStorage.removeItem('pocketbase_auth');
|
||||
}
|
||||
});
|
||||
localStorage.removeItem('pocketbase_auth');
|
||||
}
|
||||
|
||||
@@ -85,19 +85,19 @@ const ContainerMonitoring = () => {
|
||||
/>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading containers</h2>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">{t('errorLoadingContainers')}</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
|
||||
Unable to fetch container data. Please check your connection and try again.
|
||||
{t('unableToFetchContainerData')}
|
||||
</p>
|
||||
<div className="text-xs text-muted-foreground mb-4 font-mono">
|
||||
Error: {error?.message || 'Unknown error'}
|
||||
{t('errorUnknown')}: {error?.message || t('errorUnknown')}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button onClick={handleRefresh} className="text-sm sm:text-base">
|
||||
Retry
|
||||
{t('retry')}
|
||||
</Button>
|
||||
<Button onClick={handleBackToServers} variant="outline" className="text-sm sm:text-base">
|
||||
Back to Servers
|
||||
{t('backToServers')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,17 +131,17 @@ const ContainerMonitoring = () => {
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Servers
|
||||
{t('backToServers')}
|
||||
</Button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Container Monitoring
|
||||
{t('containerMonitoring')}
|
||||
</h1>
|
||||
<p className={`text-muted-foreground mt-1 sm:mt-2 transition-all duration-300 ${sidebarCollapsed ? 'text-base sm:text-lg' : 'text-sm sm:text-base'}`}>
|
||||
Monitor and manage your Docker containers in real-time
|
||||
{t('monitorAndManageContainers')}
|
||||
{serverId && (
|
||||
<span className="block text-xs text-muted-foreground/70 mt-1">
|
||||
Server ID: {serverId}
|
||||
{t('serverIdLabel')}: {serverId}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Header } from "@/components/dashboard/Header";
|
||||
@@ -11,72 +10,39 @@ import { LoadingState } from "@/components/services/LoadingState";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
|
||||
const Dashboard = () => {
|
||||
// Use shared sidebar state
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
|
||||
// Get current user
|
||||
const currentUser = authService.getCurrentUser();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// For debugging user data
|
||||
useEffect(() => {
|
||||
// console.log("Current user data:", currentUser);
|
||||
}, [currentUser]);
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
// Fetch all services with 1-minute polling for real-time updates
|
||||
const { data: services = [], isLoading, error } = useQuery({
|
||||
queryKey: ['services'],
|
||||
queryFn: serviceService.getServices,
|
||||
refetchInterval: 60000, // 1 minute as requested
|
||||
staleTime: 30000, // Data is fresh for 30 seconds
|
||||
gcTime: 120000, // Keep in cache for 2 minutes
|
||||
refetchOnWindowFocus: true, // Refetch when window gains focus
|
||||
refetchOnMount: true, // Refetch on mount
|
||||
refetchOnReconnect: true, // Refetch on reconnect
|
||||
retry: 2,
|
||||
retryDelay: 3000,
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
|
||||
// Start monitoring all active services when the dashboard loads - only once
|
||||
useEffect(() => {
|
||||
let hasStarted = false;
|
||||
|
||||
const startActiveServices = async () => {
|
||||
if (hasStarted) return;
|
||||
hasStarted = true;
|
||||
|
||||
await serviceService.startAllActiveServices();
|
||||
// console.log("Active services monitoring started");
|
||||
};
|
||||
|
||||
// Only start once and add a delay to prevent immediate execution
|
||||
const timeoutId = setTimeout(startActiveServices, 2000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, []); // Remove services dependency to prevent re-runs
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
// Show the loading state while fetching data
|
||||
if (isLoading) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex flex-col flex-1">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<DashboardContent
|
||||
services={services}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
@@ -10,16 +9,13 @@ import { ServerTable } from "@/components/servers/ServerTable";
|
||||
import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog";
|
||||
import { serverService } from "@/services/serverService";
|
||||
import { Server, ServerStats } from "@/types/server.types";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import { authService } from "@/services/authService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
const InstanceMonitoring = () => {
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [stats, setStats] = useState<ServerStats>({
|
||||
@@ -29,13 +25,13 @@ const InstanceMonitoring = () => {
|
||||
warning: 0
|
||||
});
|
||||
|
||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||
const [currentUser] = useState(authService.getCurrentUser());
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
|
||||
const { data: servers = [], isLoading, error, refetch } = useQuery({
|
||||
const { data: servers = [], isLoading, error, refetch } = useQuery<Server[]>({
|
||||
queryKey: ['servers'],
|
||||
queryFn: serverService.getServers,
|
||||
refetchInterval: 30000 // Refetch every 30 seconds
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,88 +40,38 @@ const InstanceMonitoring = () => {
|
||||
}
|
||||
}, [servers]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleAgentAdded = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading servers</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
|
||||
Unable to fetch server data. Please check your connection and try again.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="p-4 sm:p-6 lg:p-8 space-y-6">
|
||||
{/* Header Section */}
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-2xl lg:text-2xl font-bold text-foreground">
|
||||
Instance Monitoring
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
|
||||
Monitor and manage your server instances in real-time
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Server Agent
|
||||
</Button>
|
||||
<div className="p-4 lg:p-8 space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{t('instanceMonitoring')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{t('describeMonitorInstance')}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('addServerAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards Section */}
|
||||
<div>
|
||||
<ServerStatsCards stats={stats} />
|
||||
</div>
|
||||
|
||||
{/* Server Table Section */}
|
||||
<div>
|
||||
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
|
||||
</div>
|
||||
<ServerStatsCards stats={stats} />
|
||||
<ServerTable servers={servers} isLoading={isLoading} onRefresh={refetch} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -133,7 +79,7 @@ const InstanceMonitoring = () => {
|
||||
<AddServerAgentDialog
|
||||
open={addDialogOpen}
|
||||
onOpenChange={setAddDialogOpen}
|
||||
onAgentAdded={handleAgentAdded}
|
||||
onAgentAdded={refetch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -120,15 +120,15 @@ const ServerDetail = () => {
|
||||
/>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading server</h2>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">{t('errorLoadingServer')}</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
|
||||
Unable to fetch server data. Please check your connection and try again.
|
||||
{t('unableToFetchServerData')}
|
||||
</p>
|
||||
<div className="text-xs text-muted-foreground mb-4 font-mono">
|
||||
Error: {serverError?.message || 'Unknown error'}
|
||||
</div>
|
||||
<Button onClick={handleBackToServers} variant="outline" className="text-sm sm:text-base">
|
||||
Back to Servers
|
||||
{t('backToServers')}
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
@@ -151,7 +151,7 @@ const ServerDetail = () => {
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Loading server details...</p>
|
||||
<p className="text-muted-foreground">{t('loadingServerDetails')}</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -183,7 +183,7 @@ const ServerDetail = () => {
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Servers
|
||||
{t('backToServers')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
@@ -199,14 +199,17 @@ const ServerDetail = () => {
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{server?.name || 'Server Detail'}
|
||||
{server?.name || t('serverDetail')}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">
|
||||
Monitor server performance metrics and system health
|
||||
{t('monitorServerMetrics')}
|
||||
{server && (
|
||||
<span className="block text-xs text-muted-foreground/70 mt-1">
|
||||
{server.hostname} • {server.ip_address} • {server.os_type}
|
||||
{t('serverHostnameIpOs')
|
||||
.replace('{hostname}', server.hostname)
|
||||
.replace('{ip_address}', server.ip_address)
|
||||
.replace('{os_type}', server.os_type)}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
|
||||
collectionId?: string;
|
||||
collectionName?: string;
|
||||
service_id: string;
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "notifiarr" | "gotify" | "webhook";
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "notifiarr" | "gotify" | "webhook" | "matrix";
|
||||
telegram_chat_id?: string;
|
||||
discord_webhook_url?: string;
|
||||
signal_number?: string;
|
||||
@@ -34,6 +34,9 @@ export interface AlertConfiguration {
|
||||
server_url?: string;
|
||||
webhook_url?: string;
|
||||
webhook_payload_template?: string;
|
||||
matrix_homeserver?: string;
|
||||
matrix_room_id?: string;
|
||||
matrix_access_token?: string;
|
||||
}
|
||||
|
||||
export const alertConfigService = {
|
||||
@@ -105,8 +108,13 @@ export const alertConfigService = {
|
||||
} else if (config.notification_type === "webhook") {
|
||||
cleanConfig.webhook_url = config.webhook_url || "";
|
||||
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
|
||||
|
||||
}
|
||||
|
||||
} else if (config.notification_type === "matrix") {
|
||||
cleanConfig.matrix_homeserver = config.matrix_homeserver || "";
|
||||
cleanConfig.matrix_room_id = config.matrix_room_id || "";
|
||||
cleanConfig.matrix_access_token = config.matrix_access_token || "";
|
||||
|
||||
}
|
||||
const result = await pb.collection('alert_configurations').create(cleanConfig);
|
||||
|
||||
toast({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
|
||||
export interface LoginCredentials {
|
||||
@@ -98,5 +97,10 @@ export const authService = {
|
||||
} catch (error) {
|
||||
// console.error('Failed to refresh user data:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async impersonateUser(userId: string, durationSeconds: number = 3600): Promise<string | undefined> {
|
||||
const impersonateClient = await pb.collection("users").impersonate(userId, durationSeconds);
|
||||
return impersonateClient.authStore?.token;
|
||||
}
|
||||
};
|
||||
@@ -6,6 +6,8 @@ export interface ServerThreshold {
|
||||
name: string;
|
||||
cpu_threshold: number;
|
||||
ram_threshold: number;
|
||||
// Some API responses may use a different field name; include it as optional
|
||||
ram_threshold_message?: number;
|
||||
disk_threshold: number;
|
||||
network_threshold: number;
|
||||
created: string;
|
||||
|
||||
@@ -17,6 +17,7 @@ export const loginTranslations: LoginTranslations = {
|
||||
loginSuccessMessage: "Sie wurden erfolgreich angemeldet.",
|
||||
loginFailed: "Anmeldung fehlgeschlagen",
|
||||
authenticationFailed: "Authentifizierung fehlgeschlagen",
|
||||
invalidCredentials: "Ungültige E-Mail oder Passwort",
|
||||
bySigningIn: "Durch die Anmeldung stimmen Sie unseren",
|
||||
termsAndConditions: "Allgemeinen Geschäftsbedingungen",
|
||||
and: "und",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { MaintenanceTranslations } from '../types/maintenance';
|
||||
|
||||
export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
@@ -77,4 +78,12 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
selectNotificationChannel: 'Benachrichtigungskanal hinzufügen',
|
||||
enableNotificationsFirst: 'Aktivieren Sie zuerst Benachrichtigungen, um einen Kanal auszuwählen',
|
||||
updateMaintenance: 'Wartung aktualisieren',
|
||||
loginToViewProfile: 'Bitte melden Sie sich an, um Ihr Profil anzuzeigen',
|
||||
goToLogin: 'Zum Login',
|
||||
loadingUserData: 'Benutzerdaten werden geladen...',
|
||||
retry: 'Erneut versuchen',
|
||||
loadingServerData: 'Serverdaten werden geladen...',
|
||||
retrievingYourInformation: 'Bitte warten Sie, während wir Ihre Informationen abrufen...',
|
||||
servicesPagination: '{startItem}-{endItem} von {totalItems} Diensten',
|
||||
servicesPaginationNoService: '0 Dienste',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { DockerTranslations } from '../types/docker';
|
||||
|
||||
export const dockerTranslations: DockerTranslations = {
|
||||
dockerContainers: "Docker Containers",
|
||||
container: "Container",
|
||||
status: "Status",
|
||||
cpuUsage: "CPU Usage",
|
||||
memory: "Memory",
|
||||
disk: "Disk",
|
||||
uptime: "Uptime",
|
||||
lastChecked: "Last Checked",
|
||||
actions: "Actions",
|
||||
searchContainersPlaceholder: "Search containers...",
|
||||
refresh: "Refresh",
|
||||
openMenu: "Open menu",
|
||||
viewMetrics: "View Metrics",
|
||||
viewDetails: "View Details",
|
||||
|
||||
running: "Running",
|
||||
stopped: "Stopped",
|
||||
warning: "Warning",
|
||||
unknown: "Unknown",
|
||||
totalContainers: "Total Containers",
|
||||
containersLabel: "Containers",
|
||||
|
||||
noContainersFound: "No containers found",
|
||||
noContainersRunning: "No containers running",
|
||||
tryAdjustSearch: "Try adjusting your search terms.",
|
||||
startSomeContainers: "Start some containers to see them here.",
|
||||
|
||||
containerMetricsTitle: "Container Metrics: {name}",
|
||||
dockerId: "Docker ID",
|
||||
loadingMetrics: "Loading metrics...",
|
||||
errorLoadingMetrics: "Error loading metrics",
|
||||
noMetricsAvailable: "No metrics data available for this container",
|
||||
|
||||
minutes60: "60 minutes",
|
||||
day1: "1 day",
|
||||
days7: "7 days",
|
||||
month1: "1 month",
|
||||
months3: "3 months",
|
||||
|
||||
used: "Used:",
|
||||
free: "Free:",
|
||||
total: "Total:",
|
||||
usage: "Usage:",
|
||||
cpu: "CPU",
|
||||
memoryTitle: "Memory",
|
||||
diskTitle: "Disk",
|
||||
network: "Network",
|
||||
|
||||
cpuUsagePct: "CPU Usage (%)",
|
||||
cpuUsageVsAvailable: "CPU Usage vs Available",
|
||||
cpuAvailablePct: "CPU Available (%)",
|
||||
ramUsagePct: "RAM Usage (%)",
|
||||
memoryUsageBytes: "Memory Usage (Bytes)",
|
||||
usedMemory: "Used Memory",
|
||||
totalMemory: "Total Memory",
|
||||
diskUsagePct: "Disk Usage (%)",
|
||||
diskUsageBytes: "Disk Usage (Bytes)",
|
||||
usedDisk: "Used Disk",
|
||||
totalDisk: "Total Disk",
|
||||
networkTraffic: "Network Traffic",
|
||||
networkSpeedKbs: "Network Speed (KB/s)",
|
||||
rxBytes: "RX Bytes",
|
||||
txBytes: "TX Bytes",
|
||||
rxSpeedKbs: "RX Speed (KB/s)",
|
||||
txSpeedKbs: "TX Speed (KB/s)",
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ import { maintenanceTranslations } from './maintenance';
|
||||
import { incidentTranslations } from './incident';
|
||||
import { sslTranslations } from './ssl';
|
||||
import { settingsTranslations } from './settings';
|
||||
import { instanceTranslations } from './instance';
|
||||
import { operationTranslations } from './operation';
|
||||
import { regionTranslations } from './region';
|
||||
import { dockerTranslations } from './docker';
|
||||
import { publicTranslations } from './public';
|
||||
|
||||
const enTranslations: Translations = {
|
||||
common: commonTranslations,
|
||||
@@ -19,7 +24,12 @@ const enTranslations: Translations = {
|
||||
maintenance: maintenanceTranslations,
|
||||
incident: incidentTranslations,
|
||||
ssl: sslTranslations,
|
||||
settings: settingsTranslations
|
||||
settings: settingsTranslations,
|
||||
instance: instanceTranslations,
|
||||
operation: operationTranslations,
|
||||
region: regionTranslations,
|
||||
docker: dockerTranslations,
|
||||
public: publicTranslations
|
||||
};
|
||||
|
||||
export default enTranslations;
|
||||
@@ -0,0 +1,176 @@
|
||||
|
||||
import { InstanceTranslations } from '../types/instance';
|
||||
|
||||
export const instanceTranslations: InstanceTranslations = {
|
||||
|
||||
// InstanceMonitoring.tsx
|
||||
instanceMonitoring: "Instance Monitoring",
|
||||
describeMonitorInstance: "Monitor and manage your server instances in real-time",
|
||||
addServerAgent: "Add Server Agent",
|
||||
errorLoadingServers: "Error loading servers",
|
||||
unableToFetchServerData: "Unable to fetch server data. Please check your connection and try again.",
|
||||
retry: "Retry",
|
||||
|
||||
|
||||
|
||||
// ServerTable.tsx
|
||||
servers: "Servers",
|
||||
loadingServers: "Loading servers...",
|
||||
searchServersPlaceholder: "Search servers...",
|
||||
noServersFound: "No servers found",
|
||||
deleteSelected: "Delete Selected",
|
||||
deleteSelectedConfirmTitle: "Delete selected servers?",
|
||||
deleteSelectedConfirmDesc: "This action cannot be undone. This will permanently delete {count} server(s) and all of their monitoring data.",
|
||||
selectedCount: "{count} selected",
|
||||
serversDeleted: "Servers deleted",
|
||||
serversDeletedDesc: "{count} server(s) have been deleted.",
|
||||
partialSuccess: "Partial success",
|
||||
name: "Name",
|
||||
status: "Status",
|
||||
OS: "OS",
|
||||
IPAddress: "IP Address",
|
||||
CPU: "CPU",
|
||||
memory: "Memory",
|
||||
disk: "Disk",
|
||||
uptime: "Uptime",
|
||||
lastChecked: "Last Checked",
|
||||
actions: "Actions",
|
||||
viewServerDetail: "View Server Detail",
|
||||
containerMonitoring: "Container Monitoring",
|
||||
resumeMonitoring: "Resume Monitoring",
|
||||
pauseMonitoring: "Pause Monitoring",
|
||||
editServer: "Edit Server",
|
||||
deleteServer: "Delete Server",
|
||||
deleteServerConfirmTitle: "Are you sure you want to delete this server?",
|
||||
deleteServerConfirmDesc: "This action cannot be undone. This will permanently delete {name} and all of its monitoring data.",
|
||||
cancel: "Cancel",
|
||||
deleting: "Deleting...",
|
||||
delete: "Delete",
|
||||
serverDeleted: "Server deleted",
|
||||
serverDeletedDesc: "{name} has been deleted successfully.",
|
||||
error: "Error",
|
||||
deleteServerError: "Failed to delete server. Please try again.",
|
||||
serverPaused: "Server paused",
|
||||
serverResumed: "Server resumed",
|
||||
monitoringPaused: "Monitoring paused for {name}",
|
||||
monitoringResumed: "Monitoring resumed for {name}",
|
||||
pauseServerError: "Failed to pause server monitoring. Please try again.",
|
||||
resumeServerError: "Failed to resume server monitoring. Please try again.",
|
||||
|
||||
// server cards
|
||||
totalServers: "TOTAL SERVERS",
|
||||
onlineServers: "ONLINE SERVERS",
|
||||
offlineServers: "OFFLINE SERVERS",
|
||||
warningServers: "WARNING SERVERS",
|
||||
|
||||
// AddServerAgentDialog.tsx
|
||||
addServerMonitoringAgent: "Add Server Monitoring Agent",
|
||||
configureAgentDesc: "Configure a new server monitoring agent to track system metrics and performance.",
|
||||
configureAgent: "Configure Agent",
|
||||
oneClickInstall: "One-Click Install",
|
||||
dockerOneClick: "Docker One-Click",
|
||||
manualInstallation: "Manual Installation",
|
||||
validationError: "Validation Error",
|
||||
fillRequiredFields: "Please fill in all required fields.",
|
||||
serverAgentCreated: "Server Agent Created",
|
||||
serverAgentCreatedDesc: "{name} monitoring agent has been configured successfully.",
|
||||
failedToCreateAgent: "Failed to create server monitoring agent.",
|
||||
|
||||
// ServerAgentConfigForm.tsx
|
||||
serverName: "Server Name",
|
||||
serverNamePlaceholder: "e.g., web-server-01",
|
||||
serverNameDesc: "What is the name or label used as the identifier",
|
||||
serverAgentId: "Server Agent ID",
|
||||
serverAgentIdDesc: "Auto-generated unique identifier",
|
||||
operatingSystem: "Operating System",
|
||||
checkInterval: "Check Interval",
|
||||
selectInterval: "Select interval",
|
||||
interval30s: "30 seconds",
|
||||
interval1m: "1 minute",
|
||||
interval2m: "2 minutes",
|
||||
interval5m: "5 minutes",
|
||||
checkIntervalDesc: "How often to check the server and metric status",
|
||||
retryAttempts: "Retry Attempts",
|
||||
selectRetryAttempts: "Select retry attempts",
|
||||
attempt1: "1 attempt",
|
||||
attempt2: "2 attempts",
|
||||
attempt3: "3 attempts",
|
||||
attempt5: "5 attempts",
|
||||
retryAttemptsDesc: "Number of retry attempts before marking as down",
|
||||
serverToken: "Server Token",
|
||||
serverTokenDesc: "Auto-generated authentication token",
|
||||
systemUrl: "System URL",
|
||||
systemUrlDesc: "Current system API URL",
|
||||
creatingAgent: "Creating Agent...",
|
||||
createServerAgent: "Create Server Agent"
|
||||
|
||||
|
||||
,
|
||||
|
||||
// EditServerDialog.tsx
|
||||
editServerConfiguration: "Edit Server Configuration",
|
||||
serverNameLabel: "Server Name *",
|
||||
enterServerNamePlaceholder: "Enter server name",
|
||||
checkIntervalLabel: "Check Interval",
|
||||
interval10m: "10 minutes",
|
||||
maxRetriesLabel: "Max Retries",
|
||||
selectMaxRetries: "Select max retries",
|
||||
retry1: "1 retry",
|
||||
retry2: "2 retries",
|
||||
retry3: "3 retries",
|
||||
retry5: "5 retries",
|
||||
retry10: "10 retries",
|
||||
dockerMonitoring: "Docker Monitoring",
|
||||
enabled: "Enabled",
|
||||
disabled: "Disabled",
|
||||
enableNotifications: "Enable Notifications",
|
||||
notificationSettings: "Notification Settings",
|
||||
notificationChannels: "Notification Channels",
|
||||
loadingChannels: "Loading channels...",
|
||||
noChannelsAvailable: "No notification channels available",
|
||||
selectedChannels: "Selected Channels:",
|
||||
serverSetThreshold: "Server Set Threshold",
|
||||
loadingThresholds: "Loading thresholds...",
|
||||
selectServerThreshold: "Select server threshold",
|
||||
noThreshold: "No threshold (use default)",
|
||||
thresholdDetails: "Threshold Details",
|
||||
cpuThresholdPct: "CPU Threshold (%)",
|
||||
ramThresholdPct: "RAM Threshold (%)",
|
||||
diskThresholdPct: "Disk Threshold (%)",
|
||||
networkThresholdPct: "Network Threshold (%)",
|
||||
serverTemplate: "Server Template",
|
||||
loadingTemplates: "Loading templates...",
|
||||
selectServerTemplate: "Select server template",
|
||||
noTemplate: "No template (use default)",
|
||||
templateDetails: "Template Details",
|
||||
ramMessage: "RAM Message",
|
||||
cpuMessage: "CPU Message",
|
||||
diskMessage: "Disk Message",
|
||||
networkMessage: "Network Message",
|
||||
upMessage: "Up Message",
|
||||
downMessage: "Down Message",
|
||||
noMessageDefined: "No {name} message defined",
|
||||
serverUpdated: "Server updated",
|
||||
serverUpdatedDesc: "{name} has been updated successfully.",
|
||||
updating: "Updating...",
|
||||
updateServer: "Update Server",
|
||||
failedToLoadChannels: "Failed to load notification channels",
|
||||
failedToLoadTemplates: "Failed to load templates",
|
||||
failedToLoadThresholds: "Failed to load server thresholds",
|
||||
warning: "Warning",
|
||||
serverUpdatedButThresholdFailed: "Server updated but failed to update threshold values.",
|
||||
failedToUpdateServer: "Failed to update server. Please try again.",
|
||||
|
||||
// ServerHistoryCharts.tsx
|
||||
historicalPerformance: "Historical Performance",
|
||||
loading: "Loading...",
|
||||
errorLoadingChartData: "Error loading chart data",
|
||||
noHistoricalData: "No historical data available for {{timeRange}}",
|
||||
rawMetricsCount: "Raw metrics count: {{count}}",
|
||||
serverIdTimeRange: "Server ID: {{serverId}} • Time Range: {{timeRange}}",
|
||||
dataExistsOutsideRange: "Data exists but may be outside selected time range",
|
||||
noMetricsDataFound: "No metrics data found",
|
||||
dataPointsTimeRange: "{{count}} data points • {{timeRange}}",
|
||||
// updating: "Updating...",
|
||||
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export const loginTranslations: LoginTranslations = {
|
||||
loginSuccessMessage: "You have been logged in successfully.",
|
||||
loginFailed: "Login failed",
|
||||
authenticationFailed: "Authentication failed",
|
||||
invalidCredentials: "Invalid email or password",
|
||||
bySigningIn: "By signing in, you agree to our",
|
||||
termsAndConditions: "Terms & Conditions",
|
||||
and: "and",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
import { OperationTranslations } from '../types/operation';
|
||||
|
||||
export const operationTranslations: OperationTranslations = {
|
||||
|
||||
// OperationalPageContent.tsx
|
||||
failedToLoadOperationalPages: "Failed to load operational pages",
|
||||
loadingoperationalPages: "There was an error loading your operational pages. Please try again.",
|
||||
tryagain: "Try Again",
|
||||
operationalPages: "Operational Pages",
|
||||
describeOperation: "Manage your public status pages and monitor service health",
|
||||
refresh: "Refresh",
|
||||
noOperationalPagesFound: "No operational pages found",
|
||||
createYourFirstOperationalPage: "Create your first operational page to start monitoring your services and communicate status to your users.",
|
||||
totalPages: "Total Pages",
|
||||
publicPages: "Public Pages",
|
||||
operational: "Operational",
|
||||
deleteOperationalPage: "Delete Operational Page",
|
||||
deleteOperationalPageConfirm: 'Are you sure you want to delete "{title}"? This action cannot be undone and will permanently remove the operational page and all its components.',
|
||||
cancel: "Cancel",
|
||||
delete: "Delete",
|
||||
deleting: "Deleting...",
|
||||
|
||||
// EditOperationalPageDialog.tsx
|
||||
editOperationalPage: "Edit Operational Page",
|
||||
updateYourOperationalPage: "Update your operational status page settings and manage components.",
|
||||
title: "Title",
|
||||
myServiceStatusPlaceholder: "My Service Status",
|
||||
slug: "Slug",
|
||||
myServiceStatusSlugPlaceholder: "my-service-status",
|
||||
description: "Description",
|
||||
operationalPageDescriptionPlaceholder: "A brief description of your operational page",
|
||||
theme: "Theme",
|
||||
selectTheme: "Select theme",
|
||||
themeDefault: "Default",
|
||||
themeDark: "Dark",
|
||||
themeLight: "Light",
|
||||
status: "Status",
|
||||
selectStatus: "Select status",
|
||||
statusOperational: "Operational",
|
||||
statusDegraded: "Degraded Performance",
|
||||
statusMaintenance: "Under Maintenance",
|
||||
statusMajorOutage: "Major Outage",
|
||||
publicPage: "Public Page",
|
||||
makePagePublic: "Make this page publicly accessible",
|
||||
customDomainOptional: "Custom Domain (Optional)",
|
||||
customDomainPlaceholder: "status.yourdomain.com",
|
||||
customDomainDescription: "Custom domain for your status page",
|
||||
cancelUpdate: "Cancel",
|
||||
updating: "Updating...",
|
||||
updatePage: "Update Page",
|
||||
|
||||
// ComponentsSelector.tsx
|
||||
statusPageComponents: "Status Page Components",
|
||||
addMonitoringComponentsDesc: "Add monitoring components like uptime services, SSL certificates, and incident tracking",
|
||||
selectedComponents: "Selected Components",
|
||||
service: "Service",
|
||||
server: "Server",
|
||||
addComponent: "Add Component",
|
||||
componentName: "Component Name",
|
||||
componentNamePlaceholder: "e.g., Main Website",
|
||||
displayOrder: "Display Order",
|
||||
descriptionOptional: "Description (Optional)",
|
||||
descriptionPlaceholder: "Brief description of this component",
|
||||
uptimeServiceOptional: "Uptime Service (Optional)",
|
||||
selectUptimeService: "Select an uptime service",
|
||||
serverIdOptional: "Server ID (Optional)",
|
||||
serverIdPlaceholder: "server_456",
|
||||
cancelComponent: "Cancel",
|
||||
|
||||
// OperationalPageCard.tsx
|
||||
public: "Public",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
updated: "Updated",
|
||||
view: "View",
|
||||
edit: "Edit",
|
||||
|
||||
// CreateOperationalPageDialog.tsx
|
||||
createOperationalPage: "Create Operational Page",
|
||||
createOperationalPageDesc: "Create a new operational status page to monitor your services and components.",
|
||||
createPage: "Create Page",
|
||||
creating: "Creating...",
|
||||
initialStatus: "Initial Status",
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PublicTranslations } from '../types/public';
|
||||
|
||||
export const publicTranslations: PublicTranslations = {
|
||||
systemStatus: "System Status",
|
||||
allOperational: "All systems are operational",
|
||||
degradedPerformance: "Some systems are experiencing degraded performance",
|
||||
underMaintenance: "Systems are currently under maintenance",
|
||||
majorOutage: "We are experiencing a major service outage",
|
||||
statusUnknown: "Status unknown",
|
||||
autoUpdatedByHealth: "Status automatically updated based on component health",
|
||||
lastUpdatedAt: "Last updated: {time} UTC",
|
||||
liveStatusMonitoring: "Live status monitoring",
|
||||
|
||||
loadingStatusPage: "Loading Status Page",
|
||||
fetchingRealtimeStatus: "Fetching real-time system status...",
|
||||
slugLabel: "Slug",
|
||||
statusPageNotFound: "Status Page Not Found",
|
||||
notFoundDescription: "The requested status page could not be found or is not publicly accessible.",
|
||||
goBack: "Go Back",
|
||||
retry: "Retry",
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
|
||||
import { RegionTranslations } from '../types/region';
|
||||
|
||||
export const regionTranslations: RegionTranslations = {
|
||||
|
||||
//RegionalMonitoringContent.tsx
|
||||
regionalmonitoring: "Regional Monitoring",
|
||||
descriptRegionPage: "Monitor your infrastructure from multiple regions for better reliability.",
|
||||
addRegionalAgent: "Add Regional Agent",
|
||||
totalAgents: "Total Agents",
|
||||
regionalMonitoringAgents: "Regional monitoring agents",
|
||||
onlineAgents: "Online Agents",
|
||||
currentlyConnected: "Currently connected",
|
||||
offlineAgents: "Offline Agents",
|
||||
disconnectedAgents: "Disconnected agents",
|
||||
regionalAgents: "Regional Agents",
|
||||
noRegionalAgents: "No Regional Agents",
|
||||
getStartedAddAgent: "Get started by adding your first regional monitoring agent to extend your monitoring coverage.",
|
||||
addFirstAgent: "Add First Agent",
|
||||
agentRemoved: "Agent Removed",
|
||||
agentRemovedDesc: "The regional monitoring agent has been removed.",
|
||||
regionalAgentAdded: "Regional Agent Added",
|
||||
regionalAgentAddedDesc: "The regional monitoring agent has been successfully configured.",
|
||||
error: "Error",
|
||||
failedToRemoveAgent: "Failed to remove the regional monitoring agent.",
|
||||
|
||||
//AddRegionalAgentDialog.tsx
|
||||
addRegionalMonitoringAgent: "Add Regional Monitoring Agent",
|
||||
deployRegionalMonitoringAgent: "Deploy a regional monitoring agent with automatic one-click installation.",
|
||||
regionName: "Region Name",
|
||||
regionNamePlaceholder: "e.g., us-east-1, europe-west-1, asia-pacific-1",
|
||||
agentServerIpAddress: "Agent Server IP Address",
|
||||
agentIpPlaceholder: "e.g., 192.168.1.100 or your-server.example.com",
|
||||
cancel: "Cancel",
|
||||
generateInstallation: "Generate Installation",
|
||||
generating: "Generating...",
|
||||
agentConfigurationReady: "Agent Configuration Ready!",
|
||||
oneClickInstallScriptGenerated: "One-click installation script generated with automatic configuration.",
|
||||
oneClickInstallTab: "One-Click Install",
|
||||
agentDetailsTab: "Agent Details",
|
||||
manualInstallTab: "Manual Install",
|
||||
oneClickAutomaticInstallation: "One-Click Automatic Installation",
|
||||
completeInstallationDescription: "Complete installation, configuration, and service startup in one command",
|
||||
whatThisScriptDoes: "What this script does automatically:",
|
||||
scriptActionDownload: "Downloads the latest .deb package",
|
||||
scriptActionInstall: "Installs the regional monitoring agent",
|
||||
scriptActionConfigure: "Creates configuration file with your settings",
|
||||
scriptActionStart: "Starts and enables the service",
|
||||
scriptActionHealthChecks: "Runs health checks",
|
||||
runCommandOnServer: "Run this command on your target server:",
|
||||
downloadCompleteScript: "Download Complete Script",
|
||||
copyFullScript: "Copy Full Script",
|
||||
localDevelopmentNotice: "<strong>Local Development Notice:</strong> If clipboard copying fails, this is normal in local development. You can use the 'Download Complete Script' button instead or manually copy the text.",
|
||||
generatedAgentConfiguration: "Generated Agent Configuration",
|
||||
autoConfiguredDuringInstallation: "These values will be automatically configured during installation",
|
||||
agentId: "Agent ID",
|
||||
regionNameDetail: "Region Name",
|
||||
serverIp: "Server IP",
|
||||
apiEndpoint: "API Endpoint",
|
||||
authenticationToken: "Server Token",
|
||||
createAgent: "Create Agent",
|
||||
configurationFileLocation: "<strong>Configuration file location:</strong> /etc/regional-check-agent/regional-check-agent.env",
|
||||
manualInstallationSteps: "Manual Installation Steps",
|
||||
stepByStepManualInstallation: "Step-by-step manual installation process",
|
||||
step1DownloadPackage: "Step 1: Download Package",
|
||||
downloadAmd64Notice: "✅ If you’re using the amd64 architecture, please download this package.",
|
||||
downloadArm64Notice: "✅ If you’re using the arm64 architecture, please download this package.",
|
||||
step2InstallPackage: "Step 2: Install Package",
|
||||
step3ConfigureAgent: "Step 3: Configure Agent",
|
||||
copySampleConfiguration: "Copy sample configuration from https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf",
|
||||
addConfigurationValues: "Add the configuration values shown in the Agent Details tab",
|
||||
step4StartService: "Step 4: Start Service",
|
||||
step5VerifyInstallation: "Step 5: Verify Installation",
|
||||
addAnotherAgent: "Add Another Agent",
|
||||
completeSetup: "Complete Setup",
|
||||
copied: "Copied!",
|
||||
copiedDescription: "{description} copied to clipboard.",
|
||||
copyFailed: "Copy failed",
|
||||
copyFailedDescription: "Failed to copy to clipboard.",
|
||||
copyFailedLocalhost: "This is common in local development. Please manually copy the text.",
|
||||
copyFailedChrome: "Try using HTTPS or enable clipboard permissions.",
|
||||
downloaded: "Downloaded!",
|
||||
downloadedDescription: "Installation script downloaded successfully.",
|
||||
failedToCreateAgent: "Failed to create regional agent configuration.",
|
||||
|
||||
//RegionalAgentCard.tsx
|
||||
defaultBadge: "Default",
|
||||
copyAgentId: "Copy Agent ID",
|
||||
removeAgent: "Remove Agent",
|
||||
online: "Online",
|
||||
offline: "Offline",
|
||||
lastUpdated: "Last Updated:",
|
||||
activeMonitoring: "Active monitoring",
|
||||
connectionLost: "Connection lost",
|
||||
};
|
||||
@@ -88,6 +88,7 @@ export const servicesTranslations: ServicesTranslations = {
|
||||
serviceNameRequired: "Service name is required",
|
||||
urlDomainHostRequired: "URL/Domain/Host is required",
|
||||
enterValidUrlHostnameDomain: "Please enter a valid URL, hostname, or domain",
|
||||
spacesNotAllowed: "Spaces are not allowed",
|
||||
|
||||
// Dashboard
|
||||
upServices: "UP SERVICES",
|
||||
@@ -136,4 +137,65 @@ export const servicesTranslations: ServicesTranslations = {
|
||||
processing: "Processing",
|
||||
back: "Back",
|
||||
last20Checks: "Last 20 checks",
|
||||
|
||||
//OneClickInstallTab.tsx
|
||||
oneClickInstallTitle: "One-Click Install (Recommended)",
|
||||
oneClickInstallDesc: "Copy and paste this single command to install the monitoring agent instantly",
|
||||
quickInstallCommand: "Quick Install Command",
|
||||
copy: "Copy",
|
||||
runCommandOnServer: "Simply run this command on your server:",
|
||||
sshIntoServer: "SSH into your target server",
|
||||
pasteAndRun: "Paste and run the command above",
|
||||
agentInstalled: "The agent will be installed and started automatically",
|
||||
done: "Done",
|
||||
|
||||
// DockerOneClickTab.tsx
|
||||
dockerOneClickTitle: "Docker One-Click Install",
|
||||
dockerOneClickDesc: "Automated Docker container installation with system monitoring capabilities",
|
||||
dockerOneClickCommand: "Docker One-Click Command",
|
||||
dockerScriptWill: "This script will automatically:",
|
||||
dockerScriptStep1: "Download and setup the Docker monitoring agent",
|
||||
dockerScriptStep2: "Configure all required environment variables",
|
||||
dockerScriptStep3: "Start the container with proper system access",
|
||||
dockerScriptStep4: "Setup monitoring data persistence",
|
||||
directDockerTitle: "Direct Docker Run Command",
|
||||
directDockerDesc: "If you prefer to run the Docker container directly without the script",
|
||||
dockerRunCommand: "Docker Run Command",
|
||||
dockerPrerequisites: "Prerequisites for direct Docker run:",
|
||||
dockerPrereqStep1: "Docker must be installed and running",
|
||||
dockerPrereqStep2: "The operacle/checkcle-server-agent image must be available",
|
||||
dockerPrereqStep3: "Run as root or with sudo privileges",
|
||||
|
||||
// ManualInstallTab.tsx
|
||||
manualInstallTitle: "Manual Installation Steps",
|
||||
manualInstallDesc: "Step-by-step installation process",
|
||||
serverName: "Server Name",
|
||||
agentId: "Agent ID",
|
||||
osType: "OS Type",
|
||||
downloadScript: "Download the installation script",
|
||||
makeExecutable: "Make the script executable",
|
||||
runInstall: "Run the installation with your configuration",
|
||||
prerequisites: "Prerequisites:",
|
||||
prereqRoot: "Ensure you have root/sudo access on the target server",
|
||||
prereqCurl: "Make sure curl is installed for downloading files",
|
||||
prereqInternet: "Internet connection required for downloading script",
|
||||
afterInstall: "After Installation:",
|
||||
agentWillStart: "The agent will start automatically and appear in your dashboard within a few minutes.",
|
||||
|
||||
// ServerDetail.tsx
|
||||
errorLoadingServer: "Error loading server",
|
||||
unableToFetchServerData: "Unable to fetch server data. Please check your connection and try again.",
|
||||
backToServers: "Back to Servers",
|
||||
loadingServerDetails: "Loading server details...",
|
||||
serverDetail: "Server Detail",
|
||||
monitorServerMetrics: "Monitor server performance metrics and system health",
|
||||
serverHostnameIpOs: "{hostname} • {ip_address} • {os_type}",
|
||||
|
||||
// ContainerMonitoring.tsx
|
||||
errorLoadingContainers: "Error loading containers",
|
||||
unableToFetchContainerData: "Unable to fetch container data. Please check your connection and try again.",
|
||||
errorUnknown: "Unknown error",
|
||||
containerMonitoring: "Container Monitoring",
|
||||
monitorAndManageContainers: "Monitor and manage your Docker containers in real-time",
|
||||
serverIdLabel: "Server ID",
|
||||
};
|
||||
@@ -60,4 +60,118 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
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",
|
||||
|
||||
//NotificationSettings.ts
|
||||
titleNotification: "Notification Settings",
|
||||
descriptionChannelsServices: "Configure notification channels for your services",
|
||||
addChannel: "Add Channel",
|
||||
all: "All Channels",
|
||||
telegram: "Telegram",
|
||||
discord: "Discord",
|
||||
slack: "Slack",
|
||||
signal: "Signal",
|
||||
googleChat: "Google Chat",
|
||||
email: "Email",
|
||||
webhook: "Webhook",
|
||||
matrix: "Matrix",
|
||||
|
||||
// NotificationChannelDialog.tsx
|
||||
editChannel: "Edit Notification Channel",
|
||||
addChannelDialog: "Add Notification Channel",
|
||||
channelName: "Channel Name",
|
||||
channelNameDesc: "A name to identify this notification channel",
|
||||
channelType: "Channel Type",
|
||||
selectType: "Select notification type",
|
||||
enabled: "Enabled",
|
||||
enabledDesc: "Enable or disable this notification channel",
|
||||
cancel: "Cancel",
|
||||
updateChannel: "Update Channel",
|
||||
createChannel: "Create Channel",
|
||||
payloadTemplates: "Payload Templates",
|
||||
availablePlaceholders: "Available Placeholders:",
|
||||
server: "Server",
|
||||
service: "Service",
|
||||
ssl: "SSL",
|
||||
common: "Common",
|
||||
webhookUrl: "Webhook URL",
|
||||
webhookUrlDesc: "The URL where webhook notifications will be sent",
|
||||
payloadTemplate: "Payload Template (Optional)",
|
||||
payloadTemplateDesc: "JSON template for the webhook payload. Leave empty to use default template.",
|
||||
telegramChatId: "Chat ID",
|
||||
telegramChatIdDesc: "The Telegram chat ID to send notifications to",
|
||||
botToken: "Bot Token",
|
||||
botTokenDesc: "Your Telegram bot token from @BotFather",
|
||||
discordWebhookUrl: "Webhook URL",
|
||||
discordWebhookUrlDesc: "Discord webhook URL from your server settings",
|
||||
slackWebhookUrl: "Webhook URL",
|
||||
slackWebhookUrlDesc: "Slack incoming webhook URL",
|
||||
signalNumber: "Signal Number",
|
||||
signalNumberDesc: "Signal phone number to send notifications to",
|
||||
signalApiEndpoint: "Signal API Endpoint",
|
||||
signalApiEndpointDesc: "The Rest API endpoint for your Signal service",
|
||||
googleChatWebhookUrl: "Google Chat Webhook URL",
|
||||
googleChatWebhookUrlDesc: "Google Chat webhook URL from your Google Chat space",
|
||||
emailAddress: "Email Address",
|
||||
emailAddressDesc: "Email address to send notifications to",
|
||||
emailSenderName: "Sender Name",
|
||||
emailSenderNameDesc: "Display name for outgoing emails",
|
||||
smtpServer: "SMTP Server",
|
||||
// smtpPort: "SMTP Port",
|
||||
// smtpPassword: "SMTP Password",
|
||||
smtpPasswordDesc: "Password for authenticating with the SMTP server",
|
||||
ntfyEndpoint: "NTFY Endpoint",
|
||||
ntfyEndpointDesc: "The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)",
|
||||
apiToken: "API Token",
|
||||
apiTokenOptional: "API Token (Optional)",
|
||||
apiTokenDesc: "Optional API token for authentication with NTFY server",
|
||||
pushoverUserKey: "User Key",
|
||||
pushoverUserKeyDesc: "Your Pushover user key (or group key)",
|
||||
notifiarrChannelId: "Channel ID",
|
||||
notifiarrChannelIdDesc: "The Discord channel ID where notifications will be sent",
|
||||
gotifyServerUrl: "Server URL",
|
||||
gotifyServerUrlDesc: "The URL of your Gotify server",
|
||||
matrixHomeserver: "Homeserver URL",
|
||||
matrixHomeserverDesc: "The URL of your Matrix homeserver (e.g. https://matrix.org)",
|
||||
matrixRoomId: "Room ID",
|
||||
matrixRoomIdDesc: "The Matrix room ID to send notifications to (e.g. !abc123:matrix.org)",
|
||||
matrixAccessToken: "Access Token",
|
||||
matrixAccessTokenDesc: "The access token of your Matrix bot account",
|
||||
errorSaveChannel: "Failed to save notification channel",
|
||||
|
||||
channelNamePlaceholder: "My Notification Channel",
|
||||
telegramChatIdPlaceholder: "Telegram Chat ID",
|
||||
botTokenPlaceholder: "Telegram Bot Token",
|
||||
discordWebhookUrlPlaceholder: "https://discord.com/api/webhooks/...",
|
||||
slackWebhookUrlPlaceholder: "https://hooks.slack.com/services/...",
|
||||
signalNumberPlaceholder: "+1234567890",
|
||||
signalApiEndpointPlaceholder: "https://your-signal-api.com/v2/send",
|
||||
googleChatWebhookUrlPlaceholder: "https://chat.googleapis.com/v1/spaces/...",
|
||||
emailAddressPlaceholder: "notifications@example.com",
|
||||
emailSenderNamePlaceholder: "Alert System",
|
||||
smtpServerPlaceholder: "smtp.gmail.com",
|
||||
smtpPortPlaceholder: "587",
|
||||
smtpPasswordPlaceholder: "Enter your SMTP password",
|
||||
ntfyEndpointPlaceholder: "https://ntfy.sh/your-topic",
|
||||
apiTokenPlaceholder: "Enter API token",
|
||||
pushoverUserKeyPlaceholder: "Your Pushover user key",
|
||||
notifiarrChannelIdPlaceholder: "Discord Channel ID",
|
||||
gotifyServerUrlPlaceholder: "https://your-gotify-server.com",
|
||||
webhookUrlPlaceholder: "https://api.example.com/webhook",
|
||||
matrixHomeserverPlaceholder: "https://matrix.org",
|
||||
matrixRoomIdPlaceholder: "!roomid:matrix.org",
|
||||
matrixAccessTokenPlaceholder: "syt_...",
|
||||
|
||||
// DataRetentionSettings.tsx
|
||||
// permissionNotice: "Permission Notice:",
|
||||
permissionNoticeDataRetention: "As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.",
|
||||
loadingRetentionSettings: "Loading retention settings...",
|
||||
dataRetention: "Data Retention Settings",
|
||||
dataRetentionDescription: "Configure how long monitoring data is kept in the system",
|
||||
uptimeRetentionLabel: "Uptime Monitoring Retention (days)",
|
||||
uptimeRetentionHelp: "Service uptime and incident data older than this will be automatically deleted",
|
||||
serverRetentionLabel: "Server Monitoring Retention (days)",
|
||||
serverRetentionHelp: "Server metrics and process data older than this will be automatically deleted",
|
||||
lastCleanup: "Last automatic cleanup",
|
||||
// save: "Save Changes"
|
||||
|
||||
};
|
||||
@@ -15,6 +15,7 @@ export const loginTranslations: LoginTranslations = {
|
||||
loginSuccessMessage: "正常にログインしました。",
|
||||
loginFailed: "ログイン失敗",
|
||||
authenticationFailed: "認証に失敗しました",
|
||||
invalidCredentials: "メールアドレスまたはパスワードが正しくありません",
|
||||
bySigningIn: "サインインすることで、",
|
||||
termsAndConditions: "利用規約",
|
||||
and: "と",
|
||||
|
||||
@@ -64,4 +64,25 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
noScheduledMaintenance: 'スケジュール済みメンテナンスなし',
|
||||
noMaintenanceWindows: 'この期間にメンテナンスウィンドウはありません。「メンテナンス作成」ボタンをクリックして作成してください。',
|
||||
maintenanceCreatedSuccess: 'メンテナンスウィンドウが正常に作成されました',
|
||||
};
|
||||
assignedPersonnelDescription: '現在割り当てられているユーザー',
|
||||
editMaintenanceDesc: 'メンテナンス詳細を編集',
|
||||
editMaintenanceWindow: 'メンテナンス編集',
|
||||
enableNotificationsFirst: 'チャンネルを選択するには先に通知を有効にしてください',
|
||||
goToLogin: 'ログインへ',
|
||||
impactLevel: '影響レベル',
|
||||
impactLevelDescription: '影響レベルを選択',
|
||||
loadingServerData: 'サーバーデータを読み込み中...',
|
||||
loadingUserData: 'ユーザーデータを読み込み中...',
|
||||
loginToViewProfile: 'プロフィールを表示するにはログインしてください',
|
||||
mutedNotifications: '通知が無効になっています',
|
||||
notificationsAreMuted: 'このメンテナンスでは現在通知がミュートされています',
|
||||
priorityDescription: 'メンテナンス優先度を選択',
|
||||
retrievingYourInformation: '情報を取得しています。しばらくお待ちください...',
|
||||
retry: '再試行',
|
||||
selectAssignedUsers: '割り当てユーザーを選択',
|
||||
selectNotificationChannel: '通知チャンネルを追加',
|
||||
servicesPagination: '{startItem}-{endItem} / {totalItems} サービス',
|
||||
servicesPaginationNoService: '0 サービス',
|
||||
statusDescription: 'メンテナンスステータスを選択',
|
||||
updateMaintenance: 'メンテナンスを更新',
|
||||
};
|
||||
|
||||
@@ -16,4 +16,19 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
|
||||
quickTips: "គន្លឹះរហ័ស",
|
||||
releasedOn: "បានចេញផ្សាយនៅថ្ងៃទី",
|
||||
updateSchema: "ធ្វើបច្ចុប្បន្នភាព Schema",
|
||||
updateSchemaDesc: "បញ្ចូល Schema នៃការប្រមូលដោយស្វ័យប្រវត្តិ",
|
||||
mergeFieldsLabel: "បញ្ចូល fields ជាមួយនឹងការប្រមូលដែលមានស្រាប់ (សុវត្ថិភាព - រក្សាទិន្នន័យ)",
|
||||
importing: "កំពុងបញ្ចូល...",
|
||||
clickToUpdateSchema: "ចុចដើម្បីធ្វើបច្ចុប្បន្នភាព Schema",
|
||||
importSuccessful: "បញ្ចូលជោគជ័យ",
|
||||
importFailed: "បញ្ចូលបរាជ័យ",
|
||||
instructions: "ការណែនាំ",
|
||||
mergeFields: "បញ្ចូល fields",
|
||||
instructionsMergeFields: "បន្ថែម fields ថ្មីទៅក្នុងការប្រមូលដែលមានស្រាប់ដោយសុវត្ថិភាព រក្សាទុកទិន្នន័យទាំងអស់",
|
||||
instructionsCollections: "ការប្រមូលប្រព័ន្ធ (ចាប់ផ្តើមដោយ _) និងការប្រមូលអ្នកប្រើប្រាស់នឹងត្រូវរំលងដោយស្វ័យប្រវត្តិ",
|
||||
instructionsImportAuth: "មានតែអ្នកគ្រប់គ្រងដែលបានផ្ទៀងផ្ទាត់អត្តសញ្ញាណប៉ុណ្ណោះអាចធ្វើការបញ្ចូល Schema",
|
||||
collectionsUpdatedCount: "ការប្រមូលចំនួន {count} ត្រូវបានធ្វើបច្ចុប្បន្នភាព",
|
||||
collectionsCreatedCount: "ការប្រមូលចំនួន {count} ត្រូវបានបង្កើត",
|
||||
collectionsSkippedCount: "ការប្រមូលចំនួន {count} ត្រូវបានរំលង"
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { DockerTranslations } from '../types/docker';
|
||||
|
||||
export const dockerTranslations: DockerTranslations = {
|
||||
dockerContainers: "Containers Docker",
|
||||
container: "Containers",
|
||||
status: "ស្ថានភាព",
|
||||
cpuUsage: "ការប្រើប្រាស់ CPU",
|
||||
memory: "អង្គចងចាំ",
|
||||
disk: "ថាស",
|
||||
uptime: "ពេលដំណើរការ",
|
||||
lastChecked: "បានពិនិត្យចុងក្រោយ",
|
||||
actions: "សកម្មភាព",
|
||||
searchContainersPlaceholder: "ស្វែងរកContainers...",
|
||||
refresh: "ធ្វើបច្ចុប្បន្នភាព",
|
||||
openMenu: "បើកម៉ឺនុយ",
|
||||
viewMetrics: "មើលមេត្រីក",
|
||||
viewDetails: "មើលព័ត៌មានលម្អិត",
|
||||
|
||||
running: "ដំណើរការ",
|
||||
stopped: "បានបញ្ឈប់",
|
||||
warning: "ការព្រមាន",
|
||||
unknown: "មិនស្គាល់",
|
||||
totalContainers: "Containers សរុប",
|
||||
containersLabel: "Containers",
|
||||
|
||||
noContainersFound: "រកមិនឃើញContainers",
|
||||
noContainersRunning: "គ្មានContainersកំពុងដំណើរការ",
|
||||
tryAdjustSearch: "សូមព្យាយាមកែសម្រួលពាក្យស្វែងរករបស់អ្នក។",
|
||||
startSomeContainers: "ចាប់ផ្តើមContainersខ្លះៗ ដើម្បីមើលពួកវានៅទីនេះ។",
|
||||
|
||||
containerMetricsTitle: "មេត្រីកContainers: {name}",
|
||||
dockerId: "Docker ID",
|
||||
loadingMetrics: "កំពុងផ្ទុកមេត្រីក...",
|
||||
errorLoadingMetrics: "កំហុសក្នុងការផ្ទុកមេត្រីក",
|
||||
noMetricsAvailable: "គ្មានទិន្នន័យមេត្រីកសម្រាប់Containersនេះ",
|
||||
|
||||
minutes60: "៦០ នាទី",
|
||||
day1: "១ ថ្ងៃ",
|
||||
days7: "៧ ថ្ងៃ",
|
||||
month1: "១ ខែ",
|
||||
months3: "៣ ខែ",
|
||||
|
||||
used: "បានប្រើ:",
|
||||
free: "ទំនេរ:",
|
||||
total: "សរុប:",
|
||||
usage: "ការប្រើប្រាស់:",
|
||||
cpu: "CPU",
|
||||
memoryTitle: "អង្គចងចាំ",
|
||||
diskTitle: "ថាស",
|
||||
network: "បណ្តាញ",
|
||||
|
||||
cpuUsagePct: "ការប្រើប្រាស់ CPU (%)",
|
||||
cpuUsageVsAvailable: "ការប្រើប្រាស់ CPU នឹង អាចប្រើបាន",
|
||||
cpuAvailablePct: "CPU អាចប្រើបាន (%)",
|
||||
ramUsagePct: "ការប្រើប្រាស់ RAM (%)",
|
||||
memoryUsageBytes: "ការប្រើប្រាស់អង្គចងចាំ (បៃ)",
|
||||
usedMemory: "អង្គចងចាំបានប្រើ",
|
||||
totalMemory: "អង្គចងចាំសរុប",
|
||||
diskUsagePct: "ការប្រើប្រាស់ថាស (%)",
|
||||
diskUsageBytes: "ការប្រើប្រាស់ថាស (បៃ)",
|
||||
usedDisk: "ថាសបានប្រើ",
|
||||
totalDisk: "ថាសសរុប",
|
||||
networkTraffic: "ចរាចរបណ្តាញ",
|
||||
networkSpeedKbs: "ល្បឿនបណ្តាញ (KB/s)",
|
||||
rxBytes: "RX បៃ",
|
||||
txBytes: "TX បៃ",
|
||||
rxSpeedKbs: "ល្បឿន RX (KB/s)",
|
||||
txSpeedKbs: "ល្បឿន TX (KB/s)",
|
||||
};
|
||||
|
||||
|
||||
@@ -53,4 +53,13 @@ export const incidentTranslations: IncidentTranslations = {
|
||||
failedToUpdateStatus: "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពស្ថានភាព",
|
||||
inProgress: "កំពុងដំណើរការ",
|
||||
enterRootCause: 'បញ្ចូលហេតុផលដើម',
|
||||
enterIncidentTitle: "បញ្ចូលចំណងជើងឧបទ្ទវហេតុ",
|
||||
enterIncidentDescription: "បញ្ចូលការពិពណ៌នាអំពីឧបទ្ទវហេតុ",
|
||||
enterServiceId: "បញ្ចូលលេខសម្គាល់សេវាកម្ម ដោយបំបែកដោយសញ្ញាក្បៀស",
|
||||
selectAssignedUser: "ជ្រើសរើសអ្នកប្រើប្រាស់ដែលត្រូវបានចាត់តាំង",
|
||||
noAssignedUser: "ឧបទ្ទវហេតុបច្ចុប្បន្នមិនត្រូវបានចាត់តាំងទៅអ្នកប្រើប្រាស់ណាមួយទេ",
|
||||
affectedSystems: "ប្រព័ន្ធដែលរងផលប៉ះពាល់",
|
||||
enterAffectedSystems: "បញ្ចូលប្រព័ន្ធដែលរងផលប៉ះពាល់",
|
||||
separateSystemsWithComma: "បញ្ចូលប្រព័ន្ធដែលរងផលប៉ះពាល់ ដោយបំបែកដោយសញ្ញាក្បៀស",
|
||||
createIncidentDesc: "បង្កើតឧបទ្ទវហេតុថ្មី"
|
||||
};
|
||||
|
||||
@@ -9,6 +9,11 @@ import { maintenanceTranslations } from './maintenance';
|
||||
import { incidentTranslations } from './incident';
|
||||
import { sslTranslations } from './ssl';
|
||||
import { settingsTranslations } from './settings';
|
||||
import { instanceTranslations } from './instance';
|
||||
import { operationTranslations } from './operation';
|
||||
import { regionTranslations } from './region';
|
||||
import { dockerTranslations } from './docker';
|
||||
import { publicTranslations } from './public';
|
||||
|
||||
const enTranslations: Translations = {
|
||||
common: commonTranslations,
|
||||
@@ -19,7 +24,13 @@ const enTranslations: Translations = {
|
||||
maintenance: maintenanceTranslations,
|
||||
incident: incidentTranslations,
|
||||
ssl: sslTranslations,
|
||||
settings: settingsTranslations
|
||||
settings: settingsTranslations,
|
||||
instance: instanceTranslations,
|
||||
operation: operationTranslations,
|
||||
region: regionTranslations,
|
||||
docker: dockerTranslations,
|
||||
public: publicTranslations
|
||||
|
||||
};
|
||||
|
||||
export default enTranslations;
|
||||
@@ -0,0 +1,165 @@
|
||||
|
||||
import { InstanceTranslations } from '../types/instance';
|
||||
|
||||
export const instanceTranslations: InstanceTranslations = {
|
||||
|
||||
// InstanceMonitoring.tsx
|
||||
instanceMonitoring: "ការត្រួតពិនិត្យឧទាហរណ៍",
|
||||
describeMonitorInstance: "ត្រួតពិនិត្យនិងគ្រប់គ្រងឧទាហរណ៍ម៉ាស៊ីនមេរបស់អ្នកក្នុងពេលជាក់ស្តែង",
|
||||
addServerAgent: "បន្ថែមភ្នាក់ងារម៉ាស៊ីនមេ",
|
||||
errorLoadingServers: "កំហុសក្នុងការផ្ទុកម៉ាស៊ីនមេ",
|
||||
unableToFetchServerData: "មិនអាចទាញយកទិន្នន័យម៉ាស៊ីនមេបានទេ។ សូមពិនិត្យការតភ្ជាប់របស់អ្នក ហើយព្យាយាមម្តងទៀត។",
|
||||
retry: "ព្យាយាមម្តងទៀត",
|
||||
|
||||
// ServerTable.tsx
|
||||
servers: "ម៉ាស៊ីនមេ",
|
||||
loadingServers: "កំពុងផ្ទុកម៉ាស៊ីនមេ...",
|
||||
searchServersPlaceholder: "ស្វែងរកម៉ាស៊ីនមេ...",
|
||||
noServersFound: "រកមិនឃើញម៉ាស៊ីនមេ",
|
||||
name: "ឈ្មោះ",
|
||||
status: "ស្ថានភាព",
|
||||
OS: "ប្រព័ន្ធប្រតិបត្តិការ",
|
||||
IPAddress: "អាសយដ្ឋាន IP",
|
||||
CPU: "ស៊ីភីយូ",
|
||||
memory: "អង្គចងចាំ",
|
||||
disk: "ថាស",
|
||||
uptime: "ពេលវេលាដំណើរការ",
|
||||
lastChecked: "បានពិនិត្យចុងក្រោយ",
|
||||
actions: "សកម្មភាព",
|
||||
viewServerDetail: "មើលព័ត៌មានលម្អិតម៉ាស៊ីនមេ",
|
||||
containerMonitoring: "ការត្រួតពិនិត្យ Container",
|
||||
resumeMonitoring: "បន្តការត្រួតពិនិត្យ",
|
||||
pauseMonitoring: "ផ្អាកការត្រួតពិនិត្យ",
|
||||
editServer: "កែសម្រួលម៉ាស៊ីនមេ",
|
||||
deleteServer: "លុបម៉ាស៊ីនមេ",
|
||||
deleteServerConfirmTitle: "តើអ្នកប្រាកដថាចង់លុបម៉ាស៊ីនមេនេះទេ?",
|
||||
deleteServerConfirmDesc: "សកម្មភាពនេះមិនអាចត្រឡប់វិញបានទេ។ វានឹងលុប {name} និងទិន្នន័យត្រួតពិនិត្យទាំងអស់របស់វាជាអចិន្ត្រៃយ៍។",
|
||||
cancel: "បោះបង់",
|
||||
deleting: "កំពុងលុប...",
|
||||
delete: "លុប",
|
||||
serverDeleted: "ម៉ាស៊ីនមេត្រូវបានលុប",
|
||||
serverDeletedDesc: "{name} ត្រូវបានលុបដោយជោគជ័យ។",
|
||||
error: "កំហុស",
|
||||
deleteServerError: "បរាជ័យក្នុងការលុបម៉ាស៊ីនមេ។ សូមព្យាយាមម្តងទៀត។",
|
||||
serverPaused: "ម៉ាស៊ីនមេត្រូវបានផ្អាក",
|
||||
serverResumed: "ម៉ាស៊ីនមេត្រូវបានបន្ត",
|
||||
monitoringPaused: "ការត្រួតពិនិត្យត្រូវបានផ្អាកសម្រាប់ {name}",
|
||||
monitoringResumed: "ការត្រួតពិនិត្យត្រូវបានបន្តសម្រាប់ {name}",
|
||||
pauseServerError: "បរាជ័យក្នុងការផ្អាកការត្រួតពិនិត្យម៉ាស៊ីនមេ។ សូមព្យាយាមម្តងទៀត។",
|
||||
resumeServerError: "បរាជ័យក្នុងការបន្តការត្រួតពិនិត្យម៉ាស៊ីនមេ�। សូមព្យាយាមម្តងទៀត។",
|
||||
|
||||
// ServerStatsCards.tsx
|
||||
totalServers: "ម៉ាស៊ីនមេសរុប",
|
||||
onlineServers: "ម៉ាស៊ីនមេដំណើរការ",
|
||||
offlineServers: "ម៉ាស៊ីនមេផ្អាកដំណើរការ",
|
||||
warningServers: "ម៉ាស៊ីនមេដែលមានការព្រមាន",
|
||||
|
||||
// AddServerAgentDialog.tsx
|
||||
addServerMonitoringAgent: "បន្ថែមភ្នាក់ងារត្រួតពិនិត្យម៉ាស៊ីនមេ",
|
||||
configureAgentDesc: "កំណត់រចនាសម្ព័ន្ធភ្នាក់ងារត្រួតពិនិត្យម៉ាស៊ីនមេថ្មីដើម្បីតាមដានម៉ែត្រប្រព័ន្ធ និងប្រសិទ្ធភាព។",
|
||||
configureAgent: "កំណត់រចនាសម្ព័ន្ធភ្នាក់ងារ",
|
||||
oneClickInstall: "ការដំឡើងតែមួយចុច",
|
||||
dockerOneClick: "Docker ការដំឡើងតែមួយចុច",
|
||||
manualInstallation: "ការដំឡើងដោយដៃ",
|
||||
validationError: "កំហុសក្នុងការផ្ទៀងផ្ទាត់",
|
||||
fillRequiredFields: "សូមបំពេញចន្លោះទាំងអស់ដែលតម្រូវឱ្យមាន។",
|
||||
serverAgentCreated: "ភ្នាក់ងារម៉ាស៊ីនមេត្រូវបានបង្កើត",
|
||||
serverAgentCreatedDesc: "ភ្នាក់ងារត្រួតពិនិត្យ {name} ត្រូវបានកំណត់រចនាសម្ព័ន្ធដោយជោគជ័យ។",
|
||||
failedToCreateAgent: "បរាជ័យក្នុងការបង្កើតភ្នាក់ងារត្រួតពិនិត្យម៉ាស៊ីនមេ។",
|
||||
|
||||
// ServerAgentConfigForm.tsx
|
||||
serverName: "ឈ្មោះម៉ាស៊ីនមេ",
|
||||
serverNamePlaceholder: "ឧ. web-server-01",
|
||||
serverNameDesc: "ឈ្មោះ ឬស្លាកដែលប្រើជាអត្តសញ្ញាណ",
|
||||
serverAgentId: "លេខសម្គាល់ភ្នាក់ងារម៉ាស៊ីនមេ",
|
||||
serverAgentIdDesc: "លេខសម្គាល់តែមួយគត់ដែលបង្កើតដោយស្វ័យប្រវត្តិ",
|
||||
operatingSystem: "ប្រព័ន្ធប្រតិបត្តិការ",
|
||||
checkInterval: "ចន្លោះពេលពិនិត្យ",
|
||||
selectInterval: "ជ្រើសរើសចន្លោះពេល",
|
||||
interval30s: "៣០ វិនាទី",
|
||||
interval1m: "១ នាទី",
|
||||
interval2m: "២ នាទី",
|
||||
interval5m: "៥ នាទី",
|
||||
checkIntervalDesc: "ភាពញឹកញាប់នៃការពិនិត្យស្ថានភាពម៉ាស៊ីនមេ និងម៉ែត្រ",
|
||||
retryAttempts: "ការព្យាយាមឡើងវិញ",
|
||||
selectRetryAttempts: "ជ្រើសរើសចំនួនការព្យាយាមឡើងវិញ",
|
||||
attempt1: "១ ការព្យាយាម",
|
||||
attempt2: "២ ការព្យាយាម",
|
||||
attempt3: "៣ ការព្យាយាម",
|
||||
attempt5: "៥ ការព្យាយាម",
|
||||
retryAttemptsDesc: "ចំនួនការព្យាយាមឡើងវិញមុនពេលសម្គាល់ថាដាច់",
|
||||
serverToken: "សញ្ញាសម្ងាត់ម៉ាស៊ីនមេ",
|
||||
serverTokenDesc: "សញ្ញាសម្ងាត់ផ្ទៀងផ្ទាត់ដែលបង្កើតដោយស្វ័យប្រវត្តិ",
|
||||
systemUrl: "URL ប្រព័ន្ធ",
|
||||
systemUrlDesc: "URL API ប្រព័ន្ធបច្ចុប្បន្ន",
|
||||
creatingAgent: "កំពុងបង្កើតភ្នាក់ងារ...",
|
||||
createServerAgent: "បង្កើតភ្នាក់ងារម៉ាស៊ីនមេ",
|
||||
|
||||
// EditServerDialog.tsx
|
||||
editServerConfiguration: "កែសម្រួលការកំណត់រចនាសម្ព័ន្ធម៉ាស៊ីនមេ",
|
||||
serverNameLabel: "ឈ្មោះម៉ាស៊ីនមេ *",
|
||||
enterServerNamePlaceholder: "បញ្ចូលឈ្មោះម៉ាស៊ីនមេ",
|
||||
checkIntervalLabel: "ចន្លោះពេលពិនិត្យ",
|
||||
// selectInterval: "ជ្រើសរើសចន្លោះពេល",
|
||||
interval10m: "១០ នាទី",
|
||||
maxRetriesLabel: "ការព្យាយាមឡើងវិញអតិបរិមា",
|
||||
selectMaxRetries: "ជ្រើសរើសការព្យាយាមអតិបរិមា",
|
||||
retry1: "១ ដង",
|
||||
retry2: "២ ដង",
|
||||
retry3: "៣ ដង",
|
||||
retry5: "៥ ដង",
|
||||
retry10: "១០ ដង",
|
||||
dockerMonitoring: "ការត្រួតពិនិត្យ Docker",
|
||||
enabled: "បានបើក",
|
||||
disabled: "បានបិទ",
|
||||
enableNotifications: "បើកការជូនដំណឹង",
|
||||
notificationSettings: "ការកំណត់ការជូនដំណឹង",
|
||||
notificationChannels: "ឆានែលការជូនដំណឹង",
|
||||
loadingChannels: "កំពុងផ្ទុកឆានែល...",
|
||||
noChannelsAvailable: "មិនមានឆានែលការជូនដំណឹង",
|
||||
selectedChannels: "ឆានែលដែលបានជ្រើស:",
|
||||
serverSetThreshold: "កំណត់កម្រិតម៉ាស៊ីនមេ",
|
||||
loadingThresholds: "កំពុងផ្ទុកកម្រិត...",
|
||||
selectServerThreshold: "ជ្រើសរើសកម្រិតម៉ាស៊ីនមេ",
|
||||
noThreshold: "គ្មានកម្រិត (ប្រើលំនាំដើម)",
|
||||
thresholdDetails: "ព័ត៌មានលម្អិតអំពីកម្រិត",
|
||||
cpuThresholdPct: "កម្រិត CPU (%)",
|
||||
ramThresholdPct: "កម្រិត RAM (%)",
|
||||
diskThresholdPct: "កម្រិតថាស (%)",
|
||||
networkThresholdPct: "កម្រិតបណ្តាញ (%)",
|
||||
serverTemplate: "គម្រូម៉ាស៊ីនមេ",
|
||||
loadingTemplates: "កំពុងផ្ទុកគម្រូ...",
|
||||
selectServerTemplate: "ជ្រើសរើសគម្រូម៉ាស៊ីនមេ",
|
||||
noTemplate: "គ្មានគម្រូ (ប្រើលំនាំដើម)",
|
||||
templateDetails: "ព័ត៌មានលម្អិតអំពីគម្រូ",
|
||||
ramMessage: "សារ RAM",
|
||||
cpuMessage: "សារ CPU",
|
||||
diskMessage: "សារ ថាស",
|
||||
networkMessage: "សារ បណ្តាញ",
|
||||
upMessage: "សារដំណើរការ",
|
||||
downMessage: "សារផ្អាក",
|
||||
noMessageDefined: "គ្មានសារសម្រាប់ {name}",
|
||||
serverUpdated: "បានធ្វើបច្ចុប្បន្នភាពម៉ាស៊ីនមេ",
|
||||
serverUpdatedDesc: "{name} ត្រូវបានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ។",
|
||||
updating: "កំពុងធ្វើបច្ចុប្បន្នភាព...",
|
||||
updateServer: "ធ្វើបច្ចុប្បន្នភាពម៉ាស៊ីនមេ",
|
||||
failedToLoadChannels: "បានបរាជ័យក្នុងការផ្ទុកឆានែលការជូនដំណឹង",
|
||||
failedToLoadTemplates: "បានបរាជ័យក្នុងការផ្ទុកគម្រូ",
|
||||
failedToLoadThresholds: "បានបរាជ័យក្នុងការផ្ទុកកម្រិតម៉ាស៊ីនមេ",
|
||||
warning: "ការព្រមាន",
|
||||
serverUpdatedButThresholdFailed: "ម៉ាស៊ីនមេត្រូវបានធ្វើបច្ចុប្បន្នភាព ប៉ុន្តែបរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពតម្លៃកម្រិត។",
|
||||
failedToUpdateServer: "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពម៉ាស៊ីនមេ។ សូមព្យាយាមម្តងទៀត।",
|
||||
|
||||
// ServerDetail.tsx
|
||||
historicalPerformance: "សម្ដែងប្រវត្តិប្រតិបត្តិការ",
|
||||
loading: "កំពុងផ្ទុក...",
|
||||
errorLoadingChartData: "មានបញ្ហាក្នុងការផ្ទុកទិន្នន័យតារាង",
|
||||
noHistoricalData: "មិនមានទិន្នន័យចាស់សម្រាប់ {{timeRange}}",
|
||||
rawMetricsCount: "ចំនួនមេត្រដើម៖ {{count}}",
|
||||
serverIdTimeRange: "លេខសម្គាល់ម៉ាស៊ីនបម្រើ៖ {{serverId}} • ជួរពេលវេលា៖ {{timeRange}}",
|
||||
dataExistsOutsideRange: "ទិន្នន័យមាន ប៉ុន្តែអាចនៅខាងក្រៅជួរពេលវេលាដែលបានជ្រើស",
|
||||
noMetricsDataFound: "រកមិនឃើញទិន្នន័យមេត្រទេ",
|
||||
dataPointsTimeRange: "{{count}} ចំណុចទិន្នន័យ • {{timeRange}}",
|
||||
// updating: "កំពុងធ្វើបច្ចុប្បន្នភាព...",
|
||||
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export const loginTranslations: LoginTranslations = {
|
||||
loginSuccessMessage: "អ្នកបានចូលប្រើដោយជោគជ័យ។",
|
||||
loginFailed: "ការចូលបានបរាជ័យ",
|
||||
authenticationFailed: "ការផ្ទៀងផ្ទាត់បានបរាជ័យ",
|
||||
invalidCredentials: "អ៊ីមែល ឬពាក្យសម្ងាត់មិនត្រឹមត្រូវ",
|
||||
bySigningIn: "ដោយការចូល អ្នកយល់ព្រមនឹង",
|
||||
termsAndConditions: "លក្ខខណ្ឌ",
|
||||
and: "និង",
|
||||
|
||||
@@ -65,4 +65,25 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
noScheduledMaintenance: "មិនមានកាលវិភាគថែទាំទេ",
|
||||
noMaintenanceWindows: "មិនមានកាលវិភាគថែទាំសម្រាប់រយៈពេលនេះទេ។ បង្កើតមួយដោយចុចប៊ូតុង \"បង្កើតកាលវិភាគថែទាំ\"។",
|
||||
maintenanceCreatedSuccess: "កាលវិភាគថែទាំត្រូវបានបង្កើតដោយជោគជ័យ",
|
||||
editMaintenanceWindow: "កែសម្រួលការថែទាំ",
|
||||
editMaintenanceDesc: "កែសម្រួលព័ត៌មានលម្អិតនៃការថែទាំ",
|
||||
priorityDescription: "ជ្រើសរើសអាទិភាពនៃការថែទាំ",
|
||||
statusDescription: "ជ្រើសរើសស្ថានភាពនៃការថែទាំ",
|
||||
impactLevel: "កម្រិតផលប៉ះពាល់",
|
||||
impactLevelDescription: "ជ្រើសរើសកម្រិតផលប៉ះពាល់",
|
||||
selectAssignedUsers: "ជ្រើសរើសអ្នកប្រើប្រាស់ដែលត្រូវបានចាត់តាំង",
|
||||
assignedPersonnelDescription: "អ្នកប្រើប្រាស់ដែលត្រូវបានចាត់តាំងបច្ចុប្បន្ន",
|
||||
mutedNotifications: "ការជូនដំណឹងត្រូវបានបិទ",
|
||||
notificationsAreMuted: "ការជូនដំណឹងបច្ចុប្បន្នត្រូវបានបិទសម្រាប់ការថែទាំនេះ",
|
||||
selectNotificationChannel: "បន្ថែមបណ្តាញជូនដំណឹង",
|
||||
enableNotificationsFirst: "សូមបើកការជូនដំណឹងជាមុនសិន ដើម្បីជ្រើសរើសបណ្តាញ",
|
||||
updateMaintenance: "ធ្វើបច្ចុប្បន្នភាពការថែទាំ",
|
||||
loginToViewProfile: "សូមចូលគណនីដើម្បីមើលប្រវត្តិរូបរបស់អ្នក",
|
||||
goToLogin: "ចូលគណនី",
|
||||
loadingUserData: "កំពុងផ្ទុកទិន្នន័យអ្នកប្រើប្រាស់...",
|
||||
retry: "ព្យាយាមម្តងទៀត",
|
||||
loadingServerData: "កំពុងផ្ទុកទិន្នន័យម៉ាស៊ីនមេ...",
|
||||
retrievingYourInformation: "សូមរង់ចាំខណៈពេលយើងទាញយកព័ត៌មានរបស់អ្នក...",
|
||||
servicesPagination: "សេវាកម្ម {startItem}-{endItem} នៃ {totalItems} សេវាកម្ម",
|
||||
servicesPaginationNoService: "គ្មានសេវាកម្ម",
|
||||
};
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { MenuTranslations } from '../types/menu';
|
||||
|
||||
export const menuTranslations: MenuTranslations = {
|
||||
uptimeMonitoring: "ការត្រួតពិនិត្យ Uptime",
|
||||
instanceMonitoring: "ការត្រួតពិនិត្យឧបករណ៍",
|
||||
sslDomain: "SSL និងដមែន",
|
||||
uptimeMonitoring: "ត្រួតពិនិត្យ Uptime",
|
||||
instanceMonitoring: "ត្រួតពិនិត្យម៉ាស៊ីនមេ",
|
||||
sslDomain: "SSL និងដូមែន",
|
||||
scheduleIncident: "កាលវិភាគនិងឧបទ្ទវហេតុ",
|
||||
operationalPage: "ទំព័រប្រតិបត្តិការ",
|
||||
reports: "របាយការណ៍",
|
||||
regionalMonitoring: "ការត្រួតពិនិត្យតំបន់",
|
||||
regionalMonitoring: "ត្រួតពិនិត្យតំបន់",
|
||||
settingPanel: "ផ្ទាំងការកំណត់",
|
||||
generalSettings: "ការកំណត់ទូទៅ",
|
||||
userManagement: "ការគ្រប់គ្រងអ្នកប្រើប្រាស់",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
import { OperationTranslations } from '../types/operation';
|
||||
|
||||
export const operationTranslations: OperationTranslations = {
|
||||
// OperationalPageContent.tsx
|
||||
failedToLoadOperationalPages: "បរាជ័យក្នុងការផ្ទុកទំព័រប្រតិបត្តិការ",
|
||||
loadingoperationalPages: "មានបញ្ហាក្នុងការផ្ទុកទំព័រប្រតិបត្តិការរបស់អ្នក។ សូមព្យាយាមម្តងទៀត។",
|
||||
tryagain: "ព្យាយាមម្តងទៀត",
|
||||
operationalPages: "ទំព័រប្រតិបត្តិការ",
|
||||
describeOperation: "គ្រប់គ្រងទំព័រស្ថានភាពសាធារណៈរបស់អ្នក និងត្រួតពិនិត្យសុខភាពសេវាកម្ម",
|
||||
refresh: "ធ្វើបច្ចុប្បន្នភាព",
|
||||
noOperationalPagesFound: "រកមិនឃើញទំព័រប្រតិបត្តិការទេ",
|
||||
createYourFirstOperationalPage: "បង្កើតទំព័រប្រតិបត្តិការដំបូងរបស់អ្នក ដើម្បីចាប់ផ្តើមត្រួតពិនិត្យសេវាកម្មរបស់អ្នក និងប្រាស្រ័យទាក់ទងស្ថានភាពទៅកាន់អ្នកប្រើប្រាស់។",
|
||||
totalPages: "ទំព័រសរុប",
|
||||
publicPages: "ទំព័រសាធារណៈ",
|
||||
operational: "ប្រតិបត្តិការ",
|
||||
deleteOperationalPage: "លុបទំព័រប្រតិបត្តិការ",
|
||||
deleteOperationalPageConfirm: 'តើអ្នកប្រាកដថាចង់លុប "{title}" ឬ? សកម្មភាពនេះមិនអាចត្រឡប់វិញបានទេ ហើយនឹងលុបទំព័រប្រតិបត្តិការ និងសមាសធាតុទាំងអស់របស់វាជាអចិន្ត្រៃយ៍។',
|
||||
cancel: "បោះបង់",
|
||||
delete: "លុប",
|
||||
deleting: "កំពុងលុប...",
|
||||
|
||||
// EditOperationalPageDialog.tsx
|
||||
editOperationalPage: "កែសម្រួលទំព័រប្រតិបត្តិការ",
|
||||
updateYourOperationalPage: "ធ្វើបច្ចុប្បន្នភាពការកំណត់ទំព័រស្ថានភាពប្រតិបត្តិការរបស់អ្នក និងគ្រប់គ្រងសមាសធាតុ។",
|
||||
title: "ចំណងជើង",
|
||||
myServiceStatusPlaceholder: "ស្ថានភាពសេវាកម្មរបស់ខ្ញុំ",
|
||||
slug: "ចំនងជើង URL",
|
||||
myServiceStatusSlugPlaceholder: "ស្ថានភាពសេវាកម្មរបស់ខ្ញុំ",
|
||||
description: "ការពិពណ៌នា",
|
||||
operationalPageDescriptionPlaceholder: "ការពិពណ៌នាខ្លីអំពីទំព័រប្រតិបត្តិការរបស់អ្នក",
|
||||
theme: "ផ្ទៃខាងក្រោយ",
|
||||
selectTheme: "ជ្រើសរើសផ្ទៃខាងក្រោយ",
|
||||
themeDefault: "លំនាំដើម",
|
||||
themeDark: "ងងឹត",
|
||||
themeLight: "ភ្លឺ",
|
||||
status: "ស្ថានភាព",
|
||||
selectStatus: "ជ្រើសរើសស្ថានភាព",
|
||||
statusOperational: "ប្រតិបត្តិការ",
|
||||
statusDegraded: "ការសមត្ថភាពធ្លាក់ចុះ",
|
||||
statusMaintenance: "នៅក្រោមការថែទាំ",
|
||||
statusMajorOutage: "ការបាត់បង់ធំ",
|
||||
publicPage: "ទំព័រសាធារណៈ",
|
||||
makePagePublic: "ធ្វើឱ្យទំព័រនេះអាចចូលដំណើរការបានសាធារណៈ",
|
||||
customDomainOptional: "ដែនកំណត់ផ្ទាល់ខ្លួន (ជាជម្រើស)",
|
||||
customDomainPlaceholder: "status.yourdomain.com",
|
||||
customDomainDescription: "ដែនកំណត់ផ្ទាល់ខ្លួនសម្រាប់ទំព័រស្ថានភាពរបស់អ្នក",
|
||||
cancelUpdate: "បោះបង់",
|
||||
updating: "កំពុងធ្វើបច្ចុប្បន្នភាព...",
|
||||
updatePage: "ធ្វើបច្ចុប្បន្នភាពទំព័រ",
|
||||
|
||||
// ComponentsSelector.tsx
|
||||
statusPageComponents: "សមាសធាតុទំព័រស្ថានភាព",
|
||||
addMonitoringComponentsDesc: "បន្ថែមសមាសធាតុត្រួតពិនិត្យដូចជាសេវាកម្មពេលវេលាដំណើរការ, វិញ្ញាបនបត្រ SSL, និងការតាមដានឧប្បត្តិហេតុ",
|
||||
selectedComponents: "សមាសធាតុដែលបានជ្រើសរើស",
|
||||
service: "សេវាកម្ម",
|
||||
server: "ម៉ាស៊ីនមេ",
|
||||
addComponent: "បន្ថែមសមាសធាតុ",
|
||||
componentName: "ឈ្មោះសមាសធាតុ",
|
||||
componentNamePlaceholder: "ឧ. គេហទំព័រមេ",
|
||||
displayOrder: "លំដាប់បង្ហាញ",
|
||||
descriptionOptional: "ការពិពណ៌នា (ស្រេចចិត្ត)",
|
||||
descriptionPlaceholder: "ការពិពណ៌នាសង្ខេបអំពីសមាសធាតុនេះ",
|
||||
uptimeServiceOptional: "សេវាកម្មពេលវេលាដំណើរការ (ស្រេចចិត្ត)",
|
||||
selectUptimeService: "ជ្រើសរើសសេវាកម្មពេលវេលាដំណើរការ",
|
||||
serverIdOptional: "លេខសម្គាល់ម៉ាស៊ីនមេ (ស្រេចចិត្ត)",
|
||||
serverIdPlaceholder: "server_456",
|
||||
cancelComponent: "បោះបង់",
|
||||
|
||||
// OperationalPageCard.tsx
|
||||
public: "សាធារណៈ",
|
||||
yes: "បាទ/ចាស",
|
||||
no: "ទេ",
|
||||
updated: "បានធ្វើបច្ចុប្បន្នភាព",
|
||||
view: "មើល",
|
||||
edit: "កែសម្រួល",
|
||||
|
||||
// CreateOperationalPageDialog.tsx
|
||||
createOperationalPage: "បង្កើតទំព័រប្រតិបត្តិការ",
|
||||
createOperationalPageDesc: "បង្កើតទំព័រស្ថានភាពប្រតិបត្តិការថ្មីដើម្បីត្រួតពិនិត្យសេវាកម្ម និងសមាសធាតុរបស់អ្នក។",
|
||||
createPage: "បង្កើតទំព័រ",
|
||||
creating: "កំពុងបង្កើត...",
|
||||
initialStatus: "ស្ថានភាពដំបូង",
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PublicTranslations } from '../types/public';
|
||||
|
||||
export const publicTranslations: PublicTranslations = {
|
||||
systemStatus: "ស្ថានភាពប្រព័ន្ធ",
|
||||
allOperational: "ប្រព័ន្ធទាំងអស់ដំណើរការធម្មតា",
|
||||
degradedPerformance: "ខ្លះៗកំពុងមានប្រសិទ្ធភាពធ្លាក់ចុះ",
|
||||
underMaintenance: "ប្រព័ន្ធកំពុងថែទាំ",
|
||||
majorOutage: "យើងកំពុងជួបការរាំងស្ទះធ្ងន់ធ្ងរ",
|
||||
statusUnknown: "មិនស្គាល់ស្ថានភាព",
|
||||
autoUpdatedByHealth: "ស្ថានភាពត្រូវបានធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិតាមសុខភាពសមាសភាគ",
|
||||
lastUpdatedAt: "បានធ្វើបច្ចុប្បន្នភាពចុងក្រោយ: {time} UTC",
|
||||
liveStatusMonitoring: "ការត្រួតពិនិត្យស្ថានភាពផ្ទាល់",
|
||||
|
||||
loadingStatusPage: "កំពុងផ្ទុកទំព័រស្ថានភាព",
|
||||
fetchingRealtimeStatus: "កំពុងទាញយកស្ថានភាពប្រព័ន្ធពេលជាក់ស្តែង...",
|
||||
slugLabel: "Slug",
|
||||
statusPageNotFound: "រកមិនឃើញទំព័រស្ថានភាព",
|
||||
notFoundDescription: "ទំព័រស្ថានភាពដែលបានស្នើរសុំមិនអាចរកឃើញ ឬមិនអាចចូលមើលជាសាធារណៈបាន។",
|
||||
goBack: "ត្រឡប់ក្រោយ",
|
||||
retry: "ព្យាយាមម្តងទៀត",
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
|
||||
import { RegionTranslations } from '../types/region';
|
||||
|
||||
export const regionTranslations: RegionTranslations = {
|
||||
|
||||
//RegionalMonitoringContent.tsx
|
||||
regionalmonitoring: "ត្រួតពិនិត្យតំបន់",
|
||||
descriptRegionPage: "ត្រួតពិនិត្យហេដ្ឋារចនាសម្ព័ន្ធរបស់អ្នកពីតំបន់ជាច្រើនសម្រាប់ភាពជឿជាក់កាន់តែប្រសើរ។",
|
||||
addRegionalAgent: "បន្ថែមភ្នាក់ងារតំបន់",
|
||||
totalAgents: "ភ្នាក់ងារសរុប",
|
||||
regionalMonitoringAgents: "ភ្នាក់ងារត្រួតពិនិត្យតំបន់",
|
||||
onlineAgents: "ភ្នាក់ងារដំណើរការ",
|
||||
currentlyConnected: "បច្ចុប្បន្នកំពុងភ្ជាប់",
|
||||
offlineAgents: "ភ្នាក់ងារមិនដំណើរការ",
|
||||
disconnectedAgents: "ភ្នាក់ងារដែលបានផ្តាច់",
|
||||
regionalAgents: "ភ្នាក់ងារតំបន់",
|
||||
noRegionalAgents: "គ្មានភ្នាក់ងារតំបន់",
|
||||
getStartedAddAgent: "ចាប់ផ្តើមដោយបន្ថែមភ្នាក់ងារត្រួតពិនិត្យតំបន់ដំបូងរបស់អ្នក ដើម្បីពង្រីកការគ្របដណ្តប់ត្រួតពិនិត្យ។",
|
||||
addFirstAgent: "បន្ថែមភ្នាក់ងារដំបូង",
|
||||
agentRemoved: "ភ្នាក់ងារត្រូវបានដកចេញ",
|
||||
agentRemovedDesc: "ភ្នាក់ងារត្រួតពិនិត្យតំបន់ត្រូវបានដកចេញ។",
|
||||
regionalAgentAdded: "ភ្នាក់ងារតំបន់ត្រូវបានបន្ថែម",
|
||||
regionalAgentAddedDesc: "ភ្នាក់ងារត្រួតពិនិត្យតំបន់ត្រូវបានកំណត់រចនាសម្ព័ន្ធដោយជោគជ័យ។",
|
||||
error: "កំហុស",
|
||||
failedToRemoveAgent: "បរាជ័យក្នុងការដកភ្នាក់ងារត្រួតពិនិត្យតំបន់។",
|
||||
|
||||
//AddRegionalAgentDialog.tsx
|
||||
addRegionalMonitoringAgent: "បន្ថែមភ្នាក់ងារត្រួតពិនិត្យតំបន់",
|
||||
deployRegionalMonitoringAgent: "ដាក់ពង្រាយភ្នាក់ងារត្រួតពិនិត្យតំបន់ជាមួយនឹងការដំឡើងតែមួយចុចដោយស្វ័យប្រវត្តិ",
|
||||
regionName: "ឈ្មោះតំបន់",
|
||||
regionNamePlaceholder: "ឧ. us-east-1, europe-west-1, asia-pacific-1",
|
||||
agentServerIpAddress: "អាសយដ្ឋាន IP របស់ម៉ាស៊ីនមេភ្នាក់ងារ",
|
||||
agentIpPlaceholder: "ឧ. 192.168.1.100 ឬ your-server.example.com",
|
||||
cancel: "បោះបង់",
|
||||
generateInstallation: "បង្កើតការដំឡើង",
|
||||
generating: "កំពុងបង្កើត...",
|
||||
agentConfigurationReady: "ការកំណត់រចនាសម្ព័ន្ធភ្នាក់ងាររួចរាល់!",
|
||||
oneClickInstallScriptGenerated: "ស្គ្រីបដំឡើងតែមួយចុចត្រូវបានបង្កើតជាមួយនឹងការកំណត់រចនាសម្ព័ន្ធដោយស្វ័យប្រវត្តិ",
|
||||
oneClickInstallTab: "ការដំឡើងតែមួយចុច",
|
||||
agentDetailsTab: "ព័ត៌មានលម្អិតភ្នាក់ងារ",
|
||||
manualInstallTab: "ការដំឡើងដោយដៃ",
|
||||
oneClickAutomaticInstallation: "ការដំឡើងដោយស្វ័យប្រវត្តិតែមួយចុច",
|
||||
completeInstallationDescription: "បញ្ចប់ការដំឡើង ការកំណត់រចនាសម្ព័ន្ធ និងការចាប់ផ្តើមសេវាកម្មក្នុងពាក្យបញ្ជាតែមួយ",
|
||||
whatThisScriptDoes: "អ្វីដែលស្គ្រីបនេះធ្វើដោយស្វ័យប្រវត្តិ:",
|
||||
scriptActionDownload: "ទាញយកកញ្ចប់ .deb ចុងក្រោយ",
|
||||
scriptActionInstall: "ដំឡើងភ្នាក់ងារត្រួតពិនិត្យតំបន់",
|
||||
scriptActionConfigure: "បង្កើតឯកសារកំណត់រចនាសម្ព័ន្ធជាមួយនឹងការកំណត់របស់អ្នក",
|
||||
scriptActionStart: "ចាប់ផ្តើម និងបើកដំណើរការសេវាកម្ម",
|
||||
scriptActionHealthChecks: "ដំណើរការការត្រួតពិនិត្យសុខភាព",
|
||||
runCommandOnServer: "ដំណើរការពាក្យបញ្ជានេះនៅលើម៉ាស៊ីនមេគោលដៅរបស់អ្នក:",
|
||||
downloadCompleteScript: "ទាញយកស្គ្រីបពេញលេញ",
|
||||
copyFullScript: "ចម្លងស្គ្រីបពេញលេញ",
|
||||
localDevelopmentNotice: "<strong>សេចក្តីជូនដំណឹងអំពីការអភិវឌ្ឍន៍មូលដ្ឋាន:</strong> ប្រសិនបើការចម្លងទៅក្តារកៅស៊ូបរាជ័យ នេះជារឿងធម្មតានៅក្នុងការអភិវឌ្ឍន៍មូលដ្ឋាន។ អ្នកអាចប្រើប៊ូតុង 'ទាញយកស្គ្រីបពេញលេញ' ជំនួស ឬចម្លងអត្ថបទដោយដៃ។",
|
||||
generatedAgentConfiguration: "ការកំណត់រចនាសម្ព័ន្ធភ្នាក់ងារដែលបានបង្កើត",
|
||||
autoConfiguredDuringInstallation: "តម្លៃទាំងនេះនឹងត្រូវបានកំណត់រចនាសម្ព័ន្ធដោយស្វ័យប្រវត្តិក្នុងអំឡុងពេលដំឡើង",
|
||||
agentId: "លេខសម្គាល់ភ្នាក់ងារ",
|
||||
regionNameDetail: "ឈ្មោះតំបន់",
|
||||
serverIp: "អាសយដ្ឋាន IP ម៉ាស៊ីនមេ",
|
||||
apiEndpoint: "ចំណុចបញ្ចប់ API",
|
||||
authenticationToken: "សញ្ញាសម្ងាត់ផ្ទៀងផ្ទាត់",
|
||||
configurationFileLocation: "<strong>ទីតាំងឯកសារកំណត់រចនាសម្ព័ន្ធ:</strong> /etc/regional-check-agent/regional-check-agent.env",
|
||||
manualInstallationSteps: "ជំហានការដំឡើងដោយដៃ",
|
||||
stepByStepManualInstallation: "ដំណើរការដំឡើងដោយដៃជាជំហានៗ",
|
||||
step1DownloadPackage: "ជំហានទី១: ទាញយកកញ្ចប់",
|
||||
downloadAmd64Notice: "✅ ប្រសិនបើអ្នកកំពុងប្រើស្ថាបត្យកម្ម amd64 សូមទាញយកកញ្ចប់នេះ។",
|
||||
downloadArm64Notice: "✅ ប្រសិនបើអ្នកកំពុងប្រើស្ថាបត្យកម្ម arm64 សូមទាញយកកញ្ចប់នេះ�।",
|
||||
step2InstallPackage: "ជំហានទី២: ដំឡើងកញ្ចប់",
|
||||
step3ConfigureAgent: "ជំហានទី៣: កំណត់រចនាសម្ព័ន្ធភ្នាក់ងារ",
|
||||
copySampleConfiguration: "ចម្លងការកំណត់រចនាសម្ព័ន្ធគំរូពី https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf",
|
||||
addConfigurationValues: "បន្ថែមតម្លៃកំណត់រចនាសម្ព័ន្ធដែលបង្ហាញនៅក្នុងផ្ទាំងព័ត៌មានលម្អិតភ្នាក់ងារ",
|
||||
step4StartService: "ជំហានទី៤: ចាប់ផ្តើមសេវាកម្ម",
|
||||
step5VerifyInstallation: "ជំហានទី៥: ផ្ទៀងផ្ទាត់ការដំឡើង",
|
||||
addAnotherAgent: "បន្ថែមភ្នាក់ងារមួយទៀត",
|
||||
completeSetup: "បញ្ចប់ការរៀបចំ",
|
||||
copied: "បានចម្លង!",
|
||||
copiedDescription: "{description} បានចម្លងទៅក្តារកៅស៊ូ។",
|
||||
copyFailed: "ការចម្លងបរាជ័យ",
|
||||
copyFailedDescription: "បរាជ័យក្នុងការចម្លងទៅក្តារកៅស៊ូ។",
|
||||
copyFailedLocalhost: "នេះជារឿងធម្មតានៅក្នុងការអភិវឌ្ឍន៍មូលដ្ឋាន។ សូមចម្លងអត្ថបទដោយដៃ។",
|
||||
copyFailedChrome: "សូមសាកល្បងប្រើ HTTPS ឬបើកសិទ្ធិចម្លងក្តារកៅស៊ូ។",
|
||||
downloaded: "បានទាញយក!",
|
||||
downloadedDescription: "ស្គ្រីបដំឡើងបានទាញយកដោយជោគជ័យ។",
|
||||
failedToCreateAgent: "បរាជ័យក្នុងការបង្កើតការកំណត់រចនាសម្ព័ន្ធភ្នាក់ងារតំបន់។",
|
||||
|
||||
//RegionalAgentCard.tsx
|
||||
defaultBadge: "លំនាំដើម",
|
||||
copyAgentId: "ចម្លងលេខសម្គាល់ភ្នាក់ងារ",
|
||||
removeAgent: "លុបភ្នាក់ងារ",
|
||||
online: "ដំណើរការ",
|
||||
offline: "មិនដំណើរការ",
|
||||
lastUpdated: "ធ្វើបច្ចុប្បន្នភាពចុងក្រោយ:",
|
||||
activeMonitoring: "ការត្រួតពិនិត្យសកម្ម",
|
||||
connectionLost: "បាត់បង់ការតភ្ជាប់",
|
||||
|
||||
};
|
||||
@@ -2,21 +2,200 @@
|
||||
import { ServicesTranslations } from '../types/services';
|
||||
|
||||
export const servicesTranslations: ServicesTranslations = {
|
||||
serviceName: "ឈ្មោះសេវាកម្ម",
|
||||
serviceNameDesc: "បញ្ចូលឈ្មោះពិពណ៌នាសម្រាប់សេវាកម្មរបស់អ្នក",
|
||||
serviceType: "ប្រភេទសេវាកម្ម",
|
||||
serviceStatus: "ស្ថានភាពសេវាកម្ម",
|
||||
responseTime: "ពេលវេលាឆ្លើយតប",
|
||||
uptime: "ពេលវេលាដំណើរការ",
|
||||
lastChecked: "ពិនិត្យចុងក្រោយ",
|
||||
noServices: "មិនមានសេវាកម្មដែលត្រូវនឹងលក្ខណៈវិនិច្ឆ័យរបស់អ្នក។",
|
||||
currentlyMonitoring: "កំពុងតាមដានពេលនេះ",
|
||||
retry: "សាកល្បងម្ដងទៀត",
|
||||
serviceStatus: "ស្ថានភាព",
|
||||
uptime: "រយៈពេលដំណើរការ",
|
||||
lastChecked: "បានពិនិត្យចុងក្រោយ",
|
||||
noServices: "គ្មានសេវាកម្មណាមួយត្រូវនឹងលក្ខណៈវិនិច្ឆ័យតម្រងរបស់អ្នកទេ។",
|
||||
currentlyMonitoring: "កំពុងត្រួតពិនិត្យ",
|
||||
retry: "សាកល្បងម្តងទៀត",
|
||||
overview: "ទិដ្ឋភាពទូទៅ",
|
||||
newService: "សេវាកម្មថ្មី",
|
||||
rowsPerPage: "ចំនួនជួរដេកក្នុងមួយទំព័រ",
|
||||
rowsPerPage: "ជួរដេកក្នុងមួយទំព័រ",
|
||||
search: "ស្វែងរក",
|
||||
allTypes: "គ្រប់ប្រភេទ",
|
||||
createNewService: "បង្កើតសេវាកម្មថ្មី",
|
||||
createNewServiceDesc: "បញ្ចូលព័ត៌មានលម្អិតដើម្បីបង្កើតសេវាកម្មថ្មីដែលត្រូវតាមដាន។",
|
||||
createNewServiceDesc: "បំពេញព័ត៌មានលម្អិតដើម្បីបង្កើតសេវាកម្មថ្មីសម្រាប់ត្រួតពិនិត្យ។",
|
||||
|
||||
// ServiceBasicFields.tsx
|
||||
serviceName: "ឈ្មោះសេវាកម្ម",
|
||||
serviceNameDesc: "បញ្ចូលឈ្មោះដែលពិពណ៌នាអំពីសេវាកម្មរបស់អ្នក",
|
||||
|
||||
// ServiceConfigFields.tsx
|
||||
checkInterval: "ចន្លោះពេលពិនិត្យ",
|
||||
seconds: "វិនាទី",
|
||||
minute: "នាទី",
|
||||
minutes: "នាទី",
|
||||
hour: "ម៉ោង",
|
||||
hours: "ម៉ោង",
|
||||
custom: "ផ្ទាល់ខ្លួន",
|
||||
checkIntervalPlaceholder: "បញ្ចូលចន្លោះពេលជាវិនាទី",
|
||||
backToPresets: "ត្រឡប់ទៅការកំណត់ជាមុន",
|
||||
checkIntervalDesc: "ភាពញឹកញាប់នៃការពិនិត្យស្ថានភាពសេវាកម្ម",
|
||||
checkIntervalDescCustom: "បញ្ចូលចន្លោះពេលផ្ទាល់ខ្លួនជាវិនាទី (អប្បបរមា ១០ វិនាទី)",
|
||||
retryAttempts: "ការព្យាយាមឡើងវិញ",
|
||||
attempt: "ការព្យាយាម",
|
||||
attempts: "ការព្យាយាម",
|
||||
retryAttemptsDesc: "ចំនួនការព្យាយាមឡើងវិញមុននឹងចាត់ទុកថាមិនដំណើរការ",
|
||||
|
||||
// ServiceForm.tsx
|
||||
updateService: "ធ្វើបច្ចុប្បន្នភាពសេវាកម្ម",
|
||||
createService: "បង្កើតសេវាកម្ម",
|
||||
|
||||
// ServiceNotificationFields.tsx
|
||||
enableNotifications: "បើកការជូនដំណឹង",
|
||||
enableNotificationsDesc: "បើក ឬបិទការជូនដំណឹងសម្រាប់សេវាកម្មនេះ",
|
||||
notificationChannels: "បណ្តាញជូនដំណឹង",
|
||||
notificationChannelsEnabledDesc: "ជ្រើសរើសបណ្តាញជូនដំណឹងសម្រាប់សេវាកម្មនេះ",
|
||||
notificationChannelsDesc: "បើកការជូនដំណឹងជាមុនដើម្បីជ្រើសរើសបណ្តាញ",
|
||||
notificationChannelsPlaceholder: "បន្ថែមបណ្តាញជូនដំណឹង",
|
||||
alertTemplate: "ទម្រង់ជូនដំណឹង",
|
||||
alertTemplateLoading: "កំពុងផ្ទុកទម្រង់...",
|
||||
alertTemplatePlaceholder: "ជ្រើសរើសទម onderwerpជូនដំណឹង",
|
||||
alertTemplateEnabledDesc: "ជ្រើសរើសទម្រង់សម្រាប់សារជូនដំណឹង",
|
||||
alertTemplateDesc: "បើកការជូនដំណឹងជាមុនដើម្បីជ្រើសរើសទម្រង់",
|
||||
|
||||
// ServiceTypeField.tsx
|
||||
serviceType: "ប្រភេទ",
|
||||
serviceTypeHTTPDesc: "ត្រួតពិនិត្យគេហទំព័រ និង REST APIs ជាមួយពិធីការ HTTP/HTTPS",
|
||||
serviceTypePINGDesc: "ត្រួតពិនិត្យភាពអាចប្រើបានរបស់ម៉ាស៊ីនជាមួយពិធីការ PING",
|
||||
serviceTypeTCPDesc: "ត្រួតពិនិត្យការតភ្ជាប់ច្រក TCP ជាមួយពិធីការ TCP",
|
||||
serviceTypeDNSDesc: "ត្រួតពិនិត្យការដោះស្រាយ DNS",
|
||||
|
||||
// ServiceRegionalFields.tsx
|
||||
regionalMonitoring: "ការត្រួតពិនិត្យតាមតំបន់",
|
||||
regionalMonitoringDesc: "ចាត់តាំងសេវាកម្មនេះទៅភ្នាក់ងារត្រួតពិនិត្យតាមតំបន់សម្រាប់ការត្រួតពិនិត្យចែកចាយ",
|
||||
regionalAgents: "ភ្នាក់ងារតាមតំបន់",
|
||||
regionalAgentsLoading: "កំពុងផ្ទុកភ្នាក់ងារ...",
|
||||
regionalAgentsAvailablePlaceholder: "ជ្រើសរើសភ្នាក់ងារតាមតំបន់បន្ថែម...",
|
||||
regionalAgentsAllSelected: "បានជ្រើសរើសភ្នាក់ងារទាំងអស់ដែលមាន",
|
||||
regionalAgentsNoAvailable: "គ្មានភ្នាក់ងារតាមតំបន់ដែលអាចប្រើបាន",
|
||||
regionalAgentsNoOnlineAvailable: "គ្មានភ្នាក់ងារតាមតំបន់អនឡាញដែលអាចប្រើបាន",
|
||||
regionalAgentsNotFoundMessage: "រកមិនឃើញភ្នាក់ងារតាមតំបន់អនឡាញទេ។ សេវាកម្មនឹងប្រើការត្រួតពិនិត្យលំនាំដើម។",
|
||||
regionalAgentsNotSelectedMessage: "មិនបានជ្រើសរើសភ្នាក់ងារតាមតំបន់ទេ។ សេវាកម្មនឹងប្រើការត្រួតពិនិត្យលំនាំដើម។",
|
||||
|
||||
// ServiceUrlField.tsx
|
||||
targetDefault: "URL/ម៉ាស៊ីនគោលដៅ",
|
||||
targetDNS: "ឈ្មោះដែន",
|
||||
targetHTTPDesc: "បញ្ចូល URL ពេញលេញរួមទាំងពិធីការ (http:// ឬ https://)",
|
||||
targetPINGDesc: "បញ្ចូលឈ្មោះម៉ាស៊ីន ឬអាសយដ្ឋាន IP ដើម្បី ping",
|
||||
targetTCPDesc: "បញ្ចូលឈ្មោះម៉ាស៊ីន ឬអាសយដ្ឋាន IP សម្រាប់ការធ្វើតេស្តការតភ្ជាប់ TCP",
|
||||
targetTCPPortDesc: "បញ្ចូលលេខច្រកសម្រាប់ការធ្វើតេស្តការតភ្ជាប់ TCP",
|
||||
targetDNSDesc: "បញ្ចូលឈ្មោះដែនសម្រាប់ការត្រួតពិនិត្យកំណត់ត្រា DNS (A, AAAA, MX, ជាដើម)",
|
||||
targetDefaultDesc: "បញ្ចូល URL ឬឈ្មោះម៉ាស៊ីនគោលដៅសម្រាប់ការត្រួតពិនិត្យ",
|
||||
targetDefaultPlaceholder: "បញ្ចូល URL ឬឈ្មោះម៉ាស៊ីន",
|
||||
|
||||
// types.ts
|
||||
serviceNameRequired: "តម្រូវឱ្យមានឈ្មោះសេវាកម្ម",
|
||||
urlDomainHostRequired: "តម្រូវឱ្យមាន URL/ដែន/ម៉ាស៊ីន",
|
||||
enterValidUrlHostnameDomain: "សូមបញ្ចូល URL, ឈ្មោះម៉ាស៊ីន ឬដែនដែលត្រឹមត្រូវ",
|
||||
spacesNotAllowed: "មិនអនុញ្ញាតឱ្យមានចន្លោះទំនេរទេ",
|
||||
|
||||
// Dashboard
|
||||
upServices: "សេវាកម្មដំណើរការ",
|
||||
downServices: "សេវាកម្មមិនដំណើរការ",
|
||||
pausedServices: "សេវាកម្មផ្អាក",
|
||||
warningServices: "សេវាកម្មព្រមាន",
|
||||
|
||||
// ServiceRowActions.tsx
|
||||
viewDetail: "មើលលម្អិត",
|
||||
resumeMonitoring: "បន្តការត្រួតពិនិត្យ",
|
||||
pauseMonitoring: "ផ្អាកការត្រួតពិនិត្យ",
|
||||
|
||||
// IncidentTable.tsx
|
||||
responseTime: "ពេលវេលាឆ្លើយតប",
|
||||
errorMessage: "សារកំហុស",
|
||||
details: "លម្អិត",
|
||||
unmuteAlerts: "បើកសំឡេងការជូនដំណឹង",
|
||||
muteAlerts: "បិទសំឡេងការជូនដំណឹង",
|
||||
|
||||
// LastCheckedTime.tsx
|
||||
pausedAt: "ផ្អាកនៅ",
|
||||
lastCheckDetails: "លម្អិតការពិនិត្យចុងក្រោយ",
|
||||
checkedAt: "បានពិនិត្យនៅ",
|
||||
monitoringPaused: "ការត្រួតពិនិត្យត្រូវបានផ្អាក",
|
||||
noAutomaticChecks: "គ្មានការពិនិត្យដោយស្វ័យប្រវត្តិ",
|
||||
|
||||
// ServiveEditDialog.tsx
|
||||
editService: "កែសម្រួលសេវាកម្ម",
|
||||
editServiceDesc: "ធ្វើបច្ចុប្បន្នភាពលម្អិតនៃសេវាកម្មដែលត្រូវបានត្រួតពិនិត្យរបស់អ្នក។",
|
||||
|
||||
// ServiceStatsCards.tsx
|
||||
lastCheckedAt: "បានពិនិត្យចុងក្រោយនៅ {datetime}",
|
||||
avg: "មធ្យម",
|
||||
lastUpChecksCount: "ការពិនិត្យដំណើរការចុងក្រោយ {count} ដង",
|
||||
basedOnlastChecksCount: "ផ្អែកលើការពិនិត្យចុងក្រោយ {count} ដង",
|
||||
totalUptime: "រយៈពេលដំណើរការសរុប",
|
||||
totalDowntime: "រយៈពេលមិនដំណើរការសរុប",
|
||||
monitoringSettings: "ការកំណត់ការត្រួតពិនិត្យ",
|
||||
monitoringSettingsInterval: "បានពិនិត្យរៀងរាល់ {interval} វិនាទី",
|
||||
monitoringSettingsType: "ការត្រួតពិនិត្យ",
|
||||
upStatusDuration: "ដំណើរការអស់រយៈពេល {duration}",
|
||||
downStatusDuration: "មិនដំណើរការអស់រយៈពេល {duration}",
|
||||
|
||||
incidentHistory: "ប្រវត្តិឧប្បត្តិហេតុ",
|
||||
processing: "កំពុងដំណើរការ",
|
||||
back: "ត្រឡប់ក្រោយ",
|
||||
last20Checks: "ការពិនិត្យ ២០ ចុងក្រោយ",
|
||||
|
||||
//OneClickInstallTab.tsx
|
||||
oneClickInstallTitle: "ការដំឡើងតែមួយចុច (ណែនាំ)",
|
||||
oneClickInstallDesc: "ចម្លង និងបិទភ្ជាប់ពាក្យបញ្ជាតែមួយនេះដើម្បីដំឡើងភ្នាក់ងារត្រួតពិនិត្យភ្លាមៗ",
|
||||
quickInstallCommand: "ពាក្យបញ្ជាដំឡើងរហ័ស",
|
||||
copy: "ចម្លង",
|
||||
runCommandOnServer: "គ្រាន់តែដំណើរការពាក្យបញ្ជានេះនៅលើម៉ាស៊ីនមេរបស់អ្នក:",
|
||||
sshIntoServer: "ចូលទៅក្នុងម៉ាស៊ីនមេគោលដៅរបស់អ្នកតាម SSH",
|
||||
pasteAndRun: "បិទភ្ជាប់ និងដំណើរការពាក្យបញ្ជាខាងលើ",
|
||||
agentInstalled: "ភ្នាក់ងារនឹងត្រូវបានដំឡើង និងចាប់ផ្តើមដោយស្វ័យប្រវត្តិ",
|
||||
done: "រួចរាល់",
|
||||
|
||||
// DockerOneClickTab.tsx
|
||||
dockerOneClickTitle: "ការដំឡើង Docker តែមួយចុច",
|
||||
dockerOneClickDesc: "ការដំឡើង Docker Container ដោយស្វ័យប្រវត្តិជាមួយនឹងសមត្ថភាពត្រួតពិនិត្យប្រព័ន្ធ",
|
||||
dockerOneClickCommand: "ពាក្យបញ្ជា Docker តែមួយចុច",
|
||||
dockerScriptWill: "ស្គ្រីបនេះនឹងធ្វើដោយស្វ័យប្រវត្តិ:",
|
||||
dockerScriptStep1: "ទាញយក និងរៀបចំភ្នាក់ងារត្រួតពិនិត្យ Docker",
|
||||
dockerScriptStep2: "កំណត់រចនាសម្ព័ន្ធអថេរបរិស្ថានដែលតម្រូវទាំងអស់",
|
||||
dockerScriptStep3: "ចាប់ផ្តើម Container ជាមួយនឹងការចូលប្រើប្រព័ន្ធត្រឹមត្រូវ",
|
||||
dockerScriptStep4: "រៀបចំការរក្សាទុកទិន្នន័យត្រួតពិនិត្យ",
|
||||
directDockerTitle: "ពាក្យបញ្ជា Docker Run ដោយផ្ទាល់",
|
||||
directDockerDesc: "ប្រសិនបើអ្នកចូលចិត្តដំណើរការ Docker Container ដោយផ្ទាល់ដោយគ្មានស្គ្រីប",
|
||||
dockerRunCommand: "ពាក្យបញ្ជា Docker Run",
|
||||
dockerPrerequisites: "តម្រូវការជាមុនសម្រាប់ការដំណើរការ Docker ដោយផ្ទាល់:",
|
||||
dockerPrereqStep1: "Docker ត្រូវតែត្រូវបានដំឡើង និងដំណើរការ",
|
||||
dockerPrereqStep2: "រូបភាព operacle/checkcle-server-agent ត្រូវតែមាន",
|
||||
dockerPrereqStep3: "ដំណើរការជា root ឬជាមួយសិទ្ធិ sudo",
|
||||
|
||||
// ManualInstallTab.tsx
|
||||
manualInstallTitle: "ជំហានការដំឡើងដោយដៃ",
|
||||
manualInstallDesc: "ដំណើរការដំឡើងជាជំហានៗ",
|
||||
serverName: "ឈ្មោះម៉ាស៊ីនមេ",
|
||||
agentId: "លេខសម្គាល់ភ្នាក់ងារ",
|
||||
osType: "ប្រភេទប្រព័ន្ធប្រតិបត្តិការ",
|
||||
downloadScript: "ទាញយកស្គ្រីបដំឡើង",
|
||||
makeExecutable: "ធ្វើឱ្យស្គ្រីបអាចប្រតិបត្តិបាន",
|
||||
runInstall: "ដំណើរការការដំឡើងជាមួយនឹងការកំណត់រចនាសម្ព័ន្ធរបស់អ្នក",
|
||||
prerequisites: "តម្រូវការជាមុន:",
|
||||
prereqRoot: "ត្រូវប្រាកដថាអ្នកមានសិទ្ធិ root/sudo នៅលើម៉ាស៊ីនមេគោលដៅ",
|
||||
prereqCurl: "ត្រូវប្រាកដថា curl ត្រូវបានដំឡើងសម្រាប់ទាញយកឯកសារ",
|
||||
prereqInternet: "តម្រូវឱ្យមានការតភ្ជាប់អ៊ីនធឺណិតសម្រាប់ទាញយកស្គ្រីប",
|
||||
afterInstall: "បន្ទាប់ពីការដំឡើង:",
|
||||
agentWillStart: "ភ្នាក់ងារនឹងចាប់ផ្តើមដោយស្វ័យប្រវត្តិ និងនឹងបង្ហាញនៅក្នុងផ្ទាំងគ្រប់គ្រងរបស់អ្នកក្នុងរយៈពេលប៉ុន្មាននាទី។",
|
||||
|
||||
// ServerDetail.tsx
|
||||
errorLoadingServer: "កំហុសក្នុងការផ្ទុកម៉ាស៊ីនមេ",
|
||||
unableToFetchServerData: "មិនអាចទាញយកទិន្នន័យម៉ាីនមេបានទេ។ សូមពិនិត្យការតភ្ជាប់របស់អ្នក ហើយព្យាយាមម្តងទៀត។",
|
||||
backToServers: "ត្រឡប់ទៅម៉ាស៊ីនមេ",
|
||||
loadingServerDetails: "កំពុងផ្ទុកព័ត៌មានលម្អិតម៉ាស៊ីនមេ...",
|
||||
serverDetail: "ព័ត៌មានលម្អិតម៉ាស៊ីនមេ",
|
||||
monitorServerMetrics: "ត្រួតពិនិត្យMetricsប្រសិទ្ធភាពម៉ាស៊ីនមេ និងសុខភាពប្រព័ន្ធ",
|
||||
serverHostnameIpOs: "{hostname} • {ip_address} • {os_type}",
|
||||
|
||||
// ContainerMonitoring.tsx
|
||||
errorLoadingContainers: "កំហុសក្នុងការផ្ទុក Container",
|
||||
unableToFetchContainerData: "មិនអាចទាញយកទិន្នន័យ Container បានទេ?। សូមពិនិត្យការតភ្ជាប់របស់អ្នក ហើយព្យាយាមម្តងទៀត។",
|
||||
errorUnknown: "កំហុសមិនស្គាល់",
|
||||
containerMonitoring: "ការត្រួតពិនិត្យ Container",
|
||||
monitorAndManageContainers: "ត្រួតពិនិត្យ និងគ្រប់គ្រង Docker Container របស់អ្នកក្នុងពេលជាក់ស្តែង",
|
||||
serverIdLabel: "លេខសម្គាល់ម៉ាស៊ីនមេ",
|
||||
|
||||
};
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import { SettingsTranslations } from '../types/settings';
|
||||
|
||||
export const settingsTranslations: SettingsTranslations = {
|
||||
// Tabs
|
||||
// General Settings - Tabs
|
||||
systemSettings: "ការកំណត់ប្រព័ន្ធ",
|
||||
mailSettings: "ការកំណត់សំបុត្រ",
|
||||
|
||||
// System Settings
|
||||
|
||||
// General Settings - System Settings
|
||||
appName: "ឈ្មោះកម្មវិធី",
|
||||
appURL: "URL កម្មវិធី",
|
||||
senderName: "ឈ្មោះអ្នកផ្ញើ",
|
||||
senderEmail: "អាសយដ្ឋានអ៊ីមែលអ្នកផ្ញើ",
|
||||
hideControls: "លាក់ការគ្រប់គ្រង",
|
||||
|
||||
// Mail Settings
|
||||
|
||||
// General Settings - Mail Settings
|
||||
smtpSettings: "ការកំណត់រចនាសម្ព័ន្ធ SMTP",
|
||||
smtpEnabled: "បើក SMTP",
|
||||
smtpHost: "ម៉ាស៊ីន SMTP",
|
||||
@@ -23,8 +23,8 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
smtpAuthMethod: "វិធីសាស្ត្រផ្ទៀងផ្ទាត់",
|
||||
enableTLS: "បើក TLS",
|
||||
localName: "ឈ្មោះមូលដ្ឋាន",
|
||||
|
||||
// Test Email
|
||||
|
||||
// General Settings - Test Email
|
||||
testEmail: "សាកល្បងអ៊ីមែល",
|
||||
sendTestEmail: "ផ្ញើអ៊ីមែលសាកល្បង",
|
||||
emailTemplate: "គំរូអ៊ីមែល",
|
||||
@@ -37,10 +37,14 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
selectCollection: "ជ្រើសរើសបណ្តុំ",
|
||||
toEmailAddress: "ទៅអាសយដ្ឋានអ៊ីមែល",
|
||||
enterEmailAddress: "បញ្ចូលអាសយដ្ឋានអ៊ីមែល",
|
||||
send: "ផ្ញើ",
|
||||
sending: "កំពុងផ្ញើ...",
|
||||
|
||||
// Actions and status
|
||||
save: "រក្សាទុកការផ្លាស់ប្ដូរ",
|
||||
testEmailSettings: "សាកល្បងការកំណត់អ៊ីមែល",
|
||||
testEmailDescription: "សាកល្បងថាតើការកំណត់អ៊ីមែលបច្ចុប្បន្នអាចប្រើបានឬអត់",
|
||||
testEmailAlert: "នេះនឹងផ្ញើអ៊ីមែលសាកល្បងដោយប្រើការកំណត់ SMTP របស់អ្នក។ សូមប្រាកដថា SMTP ត្រូវបានកំណត់យ៉ាងត្រឹមត្រូវជាមុន។",
|
||||
|
||||
// General Settings - Actions and status
|
||||
save: "រក្សាទុកការផ្លាស់ប្តូរ",
|
||||
saving: "កំពុងរក្សាទុក...",
|
||||
settingsUpdated: "បានធ្វើបច្ចុប្បន្នភាពការកំណត់ដោយជោគជ័យ",
|
||||
errorSavingSettings: "មានបញ្ហាក្នុងការរក្សាទុកការកំណត់",
|
||||
@@ -48,5 +52,124 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
testConnection: "សាកល្បងការតភ្ជាប់",
|
||||
testingConnection: "កំពុងសាកល្បងការតភ្ជាប់...",
|
||||
connectionSuccess: "ការតភ្ជាប់ជោគជ័យ",
|
||||
connectionFailed: "ការតភ្ជាប់បរាជ័យ"
|
||||
connectionFailed: "ការតភ្ជាប់បរាជ័យ",
|
||||
|
||||
// User Management
|
||||
addUser: "បន្ថែមអ្នកប្រើប្រាស់",
|
||||
permissionNotice: "សេចក្តីជូនដំណឹងអំពីការអនុញ្ញាត:",
|
||||
permissionNoticeAddUser: "ក្នុងនាមជាអ្នកប្រើប្រាស់ជាអ្នកគ្រប់គ្រង អ្នកមិនមានសិទ្ធិចូលមើល ឬកែប្រែការកំណត់ប្រព័ន្ធ និងអ៊ីមែលទេ។ ការកំណត់ទាំងនេះអាចចូលប្រើ និងកែប្រែបានដោយអ្នកគ្រប់គ្រងជាន់ខ្ពស់តែប៉ុណ្ណោះ។ សូមទាក់ទងអ្នកគ្រប់គ្រងជាន់ខ្ពស់របស់អ្នក ប្រសិនបើអ្នកត្រូវការធ្វើការផ្លាស់ប្តូរលើការកំណត់ប្រព័ន្ធ ឬការកំណត់អ៊ីមែល។",
|
||||
loadingSettings: "កំពុងផ្ទុកការកំណត់...",
|
||||
loadingSettingsError: "កំហុសក្នុងការផ្ទុកការកំណត់",
|
||||
|
||||
//NotificationSettings.ts
|
||||
titleNotification: "ការកំណត់ការជូនដំណឹង",
|
||||
descriptionChannelsServices: "កំណត់រចនាសម្ព័ន្ធបណ្តាញជូនដំណឹងសម្រាប់សេវាកម្មរបស់អ្នក",
|
||||
addChannel: "បន្ថែមបណ្តាញ",
|
||||
all: "បណ្តាញទាំងអស់",
|
||||
telegram: "តេឡេក្រាម",
|
||||
discord: "Discord",
|
||||
slack: "Slack",
|
||||
signal: "Signal",
|
||||
googleChat: "Google Chat",
|
||||
email: "អ៊ីមែល",
|
||||
webhook: "Webhook",
|
||||
matrix: "Matrix",
|
||||
|
||||
// NotificationChannelDialog.tsx
|
||||
editChannel: "កែសម្រួលបណ្តាញជូនដំណឹង",
|
||||
addChannelDialog: "បន្ថែមបណ្តាញជូនដំណឹង",
|
||||
channelName: "ឈ្មោះបណ្តាញ",
|
||||
channelNameDesc: "ឈ្មោះសម្រាប់កំណត់អត្តសញ្ញាណបណ្តាញជូនដំណឹងនេះ",
|
||||
channelType: "ប្រភេទបណ្តាញ",
|
||||
selectType: "ជ្រើសរើសប្រភេទជូនដំណឹង",
|
||||
enabled: "បើក",
|
||||
enabledDesc: "បើក ឬបិទបណ្តាញជូនដំណឹងនេះ",
|
||||
cancel: "បោះបង់",
|
||||
updateChannel: "ធ្វើបច្ចុប្បន្នភាពបណ្តាញ",
|
||||
createChannel: "បង្កើតបណ្តាញ",
|
||||
payloadTemplates: "ទម្រង់បន្ទុក",
|
||||
availablePlaceholders: "កន្លែងទុកជាមុនដែលអាចប្រើបាន៖",
|
||||
server: "ម៉ាស៊ីនបម្រើ",
|
||||
service: "សេវាកម្ម",
|
||||
ssl: "SSL",
|
||||
common: "ទូទៅ",
|
||||
webhookUrl: "URL នៃ Webhook",
|
||||
webhookUrlDesc: "URL ដែលការជូនដំណឹង webhook នឹងត្រូវបានផ្ញើទៅ",
|
||||
payloadTemplate: "ទម្រង់បន្ទុក (ជាជម្រើស)",
|
||||
payloadTemplateDesc: "ទម្រង់ JSON សម្រាប់បន្ទុក webhook។ ទុកចោលទទេដើម្បីប្រើទម្រង់លំនាំដើម។",
|
||||
telegramChatId: "លេខសម្គាល់ជជែក",
|
||||
telegramChatIdDesc: "លេខសម្គាល់ជជែក Telegram ដើម្បីផ្ញើការជូនដំណឹងទៅ",
|
||||
botToken: "លេខសម្គាល់បុត",
|
||||
botTokenDesc: "លេខសម្គាល់បុត Telegram របស់អ្នកពី @BotFather",
|
||||
discordWebhookUrl: "URL នៃ Webhook",
|
||||
discordWebhookUrlDesc: "URL នៃ webhook របស់ Discord ពីការកំណត់ម៉ាស៊ីនបម្រើរបស់អ្នក",
|
||||
slackWebhookUrl: "URL នៃ Webhook",
|
||||
slackWebhookUrlDesc: "URL នៃ webhook ចូលរបស់ Slack",
|
||||
signalNumber: "លេខ Signal",
|
||||
signalNumberDesc: "លេខទូរស័ព្ទ Signal ដើម្បីផ្ញើការជូនដំណឹងទៅ",
|
||||
signalApiEndpoint: "ចំណុចបញ្ចប់ API របស់ Signal",
|
||||
signalApiEndpointDesc: "ចំណុចបញ្ចប់ API សម្រាប់សេវាកម្ម Signal របស់អ្នក",
|
||||
googleChatWebhookUrl: "URL នៃ Webhook របស់ Google Chat",
|
||||
googleChatWebhookUrlDesc: "URL នៃ webhook របស់ Google Chat ពីកន្លែង Google Chat របស់អ្នក",
|
||||
emailAddress: "អាសយដ្ឋានអ៊ីមែល",
|
||||
emailAddressDesc: "អាសយដ្ឋានអ៊ីមែលដើម្បីផ្ញើការជូនដំណឹងទៅ",
|
||||
emailSenderName: "ឈ្មោះអ្នកផ្ញើ",
|
||||
emailSenderNameDesc: "ឈ្មោះបង្ហាញសម្រាប់អ៊ីមែលដែលផ្ញើចេញ",
|
||||
smtpServer: "ម៉ាស៊ីនបម្រើ SMTP",
|
||||
// smtpPort: "ច្រក SMTP",
|
||||
// smtpPassword: "លេខសម្ងាត់ SMTP",
|
||||
smtpPasswordDesc: "លេខសម្ងាត់សម្រាប់ការផ្ទៀងផ្ទាត់ជាមួយម៉ាស៊ីនបម្រើ SMTP",
|
||||
ntfyEndpoint: "ចំណុចបញ្ចប់ NTFY",
|
||||
ntfyEndpointDesc: "URL នៃចំណុចបញ្ចប់ NTFY រួមទាំងប្រធានបទរបស់អ្នក (ឧ. https://ntfy.sh/checkcle)",
|
||||
apiToken: "លេខសម្គាល់ API",
|
||||
apiTokenOptional: "លេខសម្គាល់ API (ជាជម្រើស)",
|
||||
apiTokenDesc: "លេខសម្គាល់ API ជាជម្រើសសម្រាប់ការផ្ទៀងផ្ទាត់ជាមួយម៉ាស៊ីនបម្រើ NTFY",
|
||||
pushoverUserKey: "លេខសម្គាល់អ្នកប្រើប្រាស់",
|
||||
pushoverUserKeyDesc: "លេខសម្គាល់អ្នកប្រើប្រាស់ Pushover របស់អ្នក (ឬលេខសម្គាល់ក្រុម)",
|
||||
notifiarrChannelId: "លេខសម្គាល់បណ្តាញ",
|
||||
notifiarrChannelIdDesc: "លេខសម្គាល់បណ្តាញ Discord ដែលការជូនដំណឹងនឹងត្រូវបានផ្ញើទៅ",
|
||||
gotifyServerUrl: "URL នៃម៉ាស៊ីនបម្រើ",
|
||||
gotifyServerUrlDesc: "URL នៃម៉ាស៊ីនបម្រើ Gotify របស់អ្នក",
|
||||
matrixHomeserver: "Homeserver URL",
|
||||
matrixHomeserverDesc: "URL នៃ Matrix homeserver របស់អ្នក (ឧ. https://matrix.org)",
|
||||
matrixRoomId: "Room ID",
|
||||
matrixRoomIdDesc: "Matrix room ID ដែលការជូនដំណឹងនឹងត្រូវបានផ្ញើទៅ (ឧ. !abc123:matrix.org)",
|
||||
matrixAccessToken: "Access Token",
|
||||
matrixAccessTokenDesc: "Access token នៃ Matrix bot account របស់អ្នក",
|
||||
errorSaveChannel: "បរាជ័យក្នុងការរក្សាទុកបណ្តាញជូនដំណឹង",
|
||||
|
||||
channelNamePlaceholder: "ប៉ុស្តិ៍ផ្ទាល់សារជូនដំណឹងរបស់ខ្ញុំ",
|
||||
telegramChatIdPlaceholder: "ID កុំព្យូទ័រនិមិត្តសារ Telegram",
|
||||
botTokenPlaceholder: "Token Bot Telegram",
|
||||
discordWebhookUrlPlaceholder: "https://discord.com/api/webhooks/...",
|
||||
slackWebhookUrlPlaceholder: "https://hooks.slack.com/services/...",
|
||||
signalNumberPlaceholder: "+1234567890",
|
||||
signalApiEndpointPlaceholder: "https://your-signal-api.com/v2/send",
|
||||
googleChatWebhookUrlPlaceholder: "https://chat.googleapis.com/v1/spaces/...",
|
||||
emailAddressPlaceholder: "notifications@example.com",
|
||||
emailSenderNamePlaceholder: "ប្រព័ន្ធជូនដំណឹងព្រមាន",
|
||||
smtpServerPlaceholder: "smtp.gmail.com",
|
||||
smtpPortPlaceholder: "587",
|
||||
smtpPasswordPlaceholder: "បញ្ចូលពាក្យសម្ងាត់ SMTP របស់អ្នក",
|
||||
ntfyEndpointPlaceholder: "https://ntfy.sh/your-topic",
|
||||
apiTokenPlaceholder: "បញ្ចូល token API",
|
||||
pushoverUserKeyPlaceholder: "កូនសោអ្នកប្រើ Pushover របស់អ្នក",
|
||||
notifiarrChannelIdPlaceholder: "ID ប៉ុស្តិ៍ផ្ទាល់សារ Discord",
|
||||
gotifyServerUrlPlaceholder: "https://your-gotify-server.com",
|
||||
webhookUrlPlaceholder: "https://api.example.com/webhook",
|
||||
matrixHomeserverPlaceholder: "https://matrix.org",
|
||||
matrixRoomIdPlaceholder: "!roomid:matrix.org",
|
||||
matrixAccessTokenPlaceholder: "syt_...",
|
||||
|
||||
// DataRetentionSettings.tsx
|
||||
permissionNoticeDataRetention: "ជាអ្នកប្រើប្រាស់អ្នកគ្រប់គ្រង អ្នកមិនមានសិទ្ធចូលដំណើរការការកំណត់រក្សាទុកទិន្នន័យទេ។ ការកំណត់ទាំងនេះអាចត្រូវបានចូលដំណើរការនិងកែប្រែដោយតែអ្នកគ្រប់គ្រងដ៏ខ្ពស់ប៉ុណ្ណោះ។",
|
||||
loadingRetentionSettings: "កំពុងផ្ទុកការកំណត់រក្សាទុក...",
|
||||
dataRetention: "ការកំណត់រក្សាទុកទិន្នន័យ",
|
||||
dataRetentionDescription: "កំណត់រចនាសម្ព័ន្ធពេលវេលាដែលទិន្នន័យត្រួតពិនិត្យត្រូវបានរក្សាទុកនៅក្នុងប្រព័ន្ធ",
|
||||
uptimeRetentionLabel: "ការរក្សាទុកការតាមដានពេលដំណើរការ (ចំនួនថ្ងៃ)",
|
||||
uptimeRetentionHelp: "ទិន្នន័យពេលដំណើរការសេវាកម្ម និងឧប្បត្តិហេតុយូរជាងនេះនឹងត្រូវលុបដោយស្វ័យប្រវត្តិ។",
|
||||
serverRetentionLabel: "ការរក្សាទុកការតាមដានម៉ាស៊ីនមេ (ចំនួនថ្ងៃ)",
|
||||
serverRetentionHelp: "រង្វាស់ម៉ាស៊ីនមេ និងទិន្នន័យដំណើរការដែលយូរជាងនេះនឹងត្រូវលុបដោយស្វ័យប្រវត្តិ។",
|
||||
lastCleanup: "ការសម្អាតស្វ័យប្រវត្តិចុងក្រោយ",
|
||||
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export const loginTranslations: LoginTranslations = {
|
||||
loginSuccessMessage: "성공적으로 로그인되었습니다.",
|
||||
loginFailed: "로그인 실패",
|
||||
authenticationFailed: "인증 실패",
|
||||
invalidCredentials: "유효하지 않은 이메일 또는 비밀번호",
|
||||
bySigningIn: "로그인하면 다음에 동의하게 됩니다:",
|
||||
termsAndConditions: "이용약관",
|
||||
and: "및",
|
||||
|
||||
@@ -78,4 +78,12 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
selectNotificationChannel: '알림 채널 추가',
|
||||
enableNotificationsFirst: '채널을 선택하려면 먼저 알림을 활성화하세요',
|
||||
updateMaintenance: '유지보수 업데이트',
|
||||
goToLogin: '로그인으로 이동',
|
||||
loadingServerData: '서버 데이터를 불러오는 중...',
|
||||
loadingUserData: '사용자 데이터를 불러오는 중...',
|
||||
loginToViewProfile: '프로필을 보려면 로그인하세요',
|
||||
retrievingYourInformation: '정보를 가져오는 중입니다. 잠시 기다려 주세요...',
|
||||
retry: '다시 시도',
|
||||
servicesPagination: '{startItem}-{endItem} / {totalItems} 서비스',
|
||||
servicesPaginationNoService: '0 서비스',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export interface DockerTranslations {
|
||||
// Tables and headers
|
||||
dockerContainers: string;
|
||||
container: string;
|
||||
status: string;
|
||||
cpuUsage: string;
|
||||
memory: string;
|
||||
disk: string;
|
||||
uptime: string;
|
||||
lastChecked: string;
|
||||
actions: string;
|
||||
searchContainersPlaceholder: string;
|
||||
refresh: string;
|
||||
openMenu: string;
|
||||
viewMetrics: string;
|
||||
viewDetails: string;
|
||||
|
||||
// Status badges and stats
|
||||
running: string;
|
||||
stopped: string;
|
||||
warning: string;
|
||||
unknown: string;
|
||||
totalContainers: string;
|
||||
containersLabel: string;
|
||||
|
||||
// Empty states
|
||||
noContainersFound: string;
|
||||
noContainersRunning: string;
|
||||
tryAdjustSearch: string;
|
||||
startSomeContainers: string;
|
||||
|
||||
// Metrics dialog
|
||||
containerMetricsTitle: string; // e.g., Container Metrics: {name}
|
||||
dockerId: string; // Docker ID
|
||||
loadingMetrics: string;
|
||||
errorLoadingMetrics: string;
|
||||
noMetricsAvailable: string;
|
||||
|
||||
// Time ranges
|
||||
minutes60: string;
|
||||
day1: string;
|
||||
days7: string;
|
||||
month1: string;
|
||||
months3: string;
|
||||
|
||||
// Metric labels
|
||||
used: string;
|
||||
free: string;
|
||||
total: string;
|
||||
usage: string;
|
||||
cpu: string;
|
||||
memoryTitle: string;
|
||||
diskTitle: string;
|
||||
network: string;
|
||||
|
||||
// Charts and series
|
||||
cpuUsagePct: string;
|
||||
cpuUsageVsAvailable: string;
|
||||
cpuAvailablePct: string;
|
||||
ramUsagePct: string;
|
||||
memoryUsageBytes: string;
|
||||
usedMemory: string;
|
||||
totalMemory: string;
|
||||
diskUsagePct: string;
|
||||
diskUsageBytes: string;
|
||||
usedDisk: string;
|
||||
totalDisk: string;
|
||||
networkTraffic: string;
|
||||
networkSpeedKbs: string;
|
||||
rxBytes: string;
|
||||
txBytes: string;
|
||||
rxSpeedKbs: string;
|
||||
txSpeedKbs: string;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user