Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bda862e4d3 | |||
| 6887dc291b | |||
| 29daddfc3b | |||
| 9c44ba6d7f | |||
| c61a740725 | |||
| a9d3d86394 | |||
| 9b0b2663ec | |||
| 0b04034141 | |||
| 0a635ab3aa | |||
| 175d2b6329 | |||
| 3d6c4151b8 | |||
| 5f893e3b20 | |||
| 1296cf6a32 | |||
| 31e5335aac | |||
| b6d4ca5269 | |||
| 7e7522608a | |||
| 43957f65d2 | |||
| 09ece0a77e | |||
| 06ba38aa15 | |||
| abd878fe6c | |||
| f4d47bbd45 | |||
| b675fddc92 | |||
| 0a70ed2143 | |||
| 4e07279a54 | |||
| 020cdec912 | |||
| 4771b2ead0 | |||
| 87c829e100 | |||
| e6093f9fe2 | |||
| 956786a859 | |||
| 6a0169e4d8 | |||
| 92959945f3 | |||
| b2269187d8 | |||
| 05f46969c4 | |||
| ab168b440e | |||
| 2e866d7a95 | |||
| b372131978 | |||
| 5f7c451fc0 | |||
| 3d7172d9aa | |||
| 22d4597f88 | |||
| d4423e328b | |||
| b3b64806f6 | |||
| 001f6f8783 | |||
| d01f682a83 | |||
| bce523bdf1 | |||
| 623e3d5ebf | |||
| e3a7865e40 | |||
| 3c93354c7d | |||
| 323ac36b65 | |||
| c437704007 | |||
| 1953d54dc5 | |||
| b574073294 | |||
| b164602ee5 | |||
| 4bd08efc9a | |||
| 58fa09bea7 | |||
| 7ff180bea4 | |||
| 7b18806066 | |||
| d4434a64e1 | |||
| 08580794da | |||
| 70fdcc9ea6 | |||
| aba3120235 | |||
| b038d87848 | |||
| 87588a9127 | |||
| 4090fa5aef | |||
| c04561dd99 | |||
| 7f1dfcdd42 | |||
| 6f753f2901 | |||
| c39296cee6 | |||
| a1a8a8443e | |||
| f866d36db4 | |||
| 98ec911281 | |||
| 046db61326 | |||
| 3c00d0933e | |||
| 8e013f0b0b | |||
| f686134525 | |||
| 98d33a609e | |||
| d33244956e | |||
| 572c572c14 | |||
| f9743407a4 | |||
| fa3ad9b2e1 | |||
| 6bf75b2bf7 | |||
| b068c0cd51 | |||
| 92617038f0 | |||
| 40488a5a53 | |||
| 35fe0b9071 | |||
| 3bc1fd7c02 | |||
| ee4ce50193 | |||
| 67a0d1d7f4 | |||
| f006df3b02 | |||
| 5234303565 | |||
| 4ff0457966 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [tolaleng]
|
||||
github: #[tolaleng]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: #checkcle
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
@@ -85,7 +85,8 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Please confirm the following before submitting:
|
||||
description: |
|
||||
Please confirm the following before submitting:
|
||||
options:
|
||||
- label: I have searched existing issues to avoid creating duplicates.
|
||||
required: true
|
||||
|
||||
@@ -51,7 +51,8 @@ body:
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting, please confirm:
|
||||
description: |
|
||||
Before submitting, please confirm:
|
||||
options:
|
||||
- label: I have searched existing issues to make sure this feature hasn’t been requested yet.
|
||||
required: true
|
||||
|
||||
@@ -45,7 +45,8 @@ body:
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting, please confirm the following:
|
||||
description: |
|
||||
Before submitting, please confirm the following:
|
||||
options:
|
||||
- label: I have searched existing issues and discussions for a solution.
|
||||
required: true
|
||||
|
||||
@@ -22,6 +22,8 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
pocketbase.exe
|
||||
application/src/lib/pocketbase.ts
|
||||
server/pb_data/
|
||||
Dockerfile
|
||||
application/src/lib/pocketbase-prod.ts
|
||||
|
||||
@@ -128,32 +128,30 @@ docker run -d \
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
🙏 We're incredibly thankful to our sponsors. Your contributions enable us to maintain infrastructure (hosting, domains), run tests, and continue developing valuable features.
|
||||
Sponsorships will no longer be accepted. From now on, support will only be accepted in the form of ecosystem and community partnerships that provide infrastructure such as cloud servers, domains, or hosting credits.
|
||||
|
||||
### 🥈 Silver Uptime Ally
|
||||
|
||||
<a href="https://github.com/michaelortnerit">
|
||||
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
||||
</a>
|
||||
|
||||
### 🧡 Ping Supporter
|
||||
|
||||
<a href="https://github.com/samang-dauth">
|
||||
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
||||
</a>
|
||||
If you’re a tech company interested in supporting CheckCle, please contact the author directly at tolaleng@checkcle.io
|
||||
|
||||
### 🤝 Ecosystem & Community Partner
|
||||
<a href="https://github.com/gitbookio" style="display: inline-block; margin-right: 10px; vertical-align: middle;">
|
||||
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
|
||||
width="75" height="75"
|
||||
style="border-radius: 50%;" />
|
||||
</a>
|
||||
<a href="https://m.do.co/c/0c27ef82475f" style="display: inline-block; vertical-align: middle;">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg"
|
||||
height="45"
|
||||
alt="DigitalOcean Logo">
|
||||
</a>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<a href="https://github.com/gitbookio">
|
||||
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
|
||||
width="75" height="75"
|
||||
style="border-radius: 50%;"
|
||||
alt="GitBook Logo" />
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com">
|
||||
<img src="https://cdn.checkcle.io/images/sponsor/cloudflare-checkcle_logo.png"
|
||||
height="60"
|
||||
alt="Cloudflare Logo" />
|
||||
</a>
|
||||
<a href="https://m.do.co/c/0c27ef82475f">
|
||||
<img src="https://cdn.checkcle.io/images/sponsor/digitalocean_checkcle.png"
|
||||
height="50"
|
||||
alt="DigitalOcean Logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
+24
-20
@@ -1,25 +1,29 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CheckCle</title>
|
||||
<meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
|
||||
<meta name="author" content="Tola Leng" />
|
||||
|
||||
<meta property="og:title" content="checkcle-an-open-source" />
|
||||
<meta property="og:description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CheckCle - Dev</title>
|
||||
<meta name="description"
|
||||
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
|
||||
<meta name="author" content="Tola Leng" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@checkcle_oss" />
|
||||
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
||||
</head>
|
||||
<meta property="og:title" content="checkcle-an-open-source" />
|
||||
<meta property="og:description"
|
||||
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@checkcle_oss" />
|
||||
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Generated
+469
-15
@@ -8,6 +8,7 @@
|
||||
"name": "vite_react_shadcn_ts",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "2.2.7",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@@ -27,7 +28,7 @@
|
||||
"@radix-ui/react-radio-group": "^1.2.0",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slider": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
@@ -43,6 +44,8 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"pocketbase": "^0.19.0",
|
||||
@@ -91,13 +94,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
|
||||
"integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -645,6 +645,21 @@
|
||||
"@floating-ui/utils": "^0.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
@@ -664,6 +679,26 @@
|
||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
|
||||
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.20.2",
|
||||
"@react-aria/interactions": "^3.25.0",
|
||||
"@tanstack/react-virtual": "^3.13.9",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
|
||||
@@ -1811,12 +1846,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
|
||||
"integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0"
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -1833,6 +1868,62 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
|
||||
@@ -2225,6 +2316,103 @@
|
||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-aria/focus": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
|
||||
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/interactions": "^3.25.5",
|
||||
"@react-aria/utils": "^3.30.1",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/interactions": {
|
||||
"version": "3.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
|
||||
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.30.1",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/utils": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
|
||||
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-stately/utils": "^3.10.8",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/flags": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
||||
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/utils": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
|
||||
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/shared": {
|
||||
"version": "3.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
|
||||
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
|
||||
@@ -2674,6 +2862,15 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
|
||||
@@ -2738,6 +2935,33 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||
@@ -2832,6 +3056,13 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
||||
@@ -2853,6 +3084,13 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
|
||||
@@ -3206,6 +3444,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
@@ -3250,6 +3500,16 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -3318,6 +3578,18 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"btoa": "bin/btoa.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -3358,6 +3630,26 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -3843,6 +4135,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.45.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
|
||||
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3856,6 +4160,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -4064,6 +4378,16 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -4424,6 +4748,12 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4647,6 +4977,20 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4848,6 +5192,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
|
||||
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.7",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf-autotable": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
|
||||
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jspdf": "^2 || ^3"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -5208,6 +5579,13 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -5450,6 +5828,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@@ -5708,10 +6096,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
@@ -5750,6 +6139,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
|
||||
@@ -5883,6 +6282,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -6039,6 +6448,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
||||
@@ -6095,6 +6520,16 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -6308,12 +6743,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vaul": {
|
||||
"version": "0.9.9",
|
||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "2.2.7",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@@ -45,6 +46,7 @@
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"framer-motion": "12.23.12",
|
||||
"input-otp": "^1.2.4",
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
+139
-45
@@ -1,37 +1,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import React, { useState } from "react";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { LanguageProvider } from './contexts/LanguageContext';
|
||||
import { SidebarProvider } from './contexts/SidebarContext';
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { LanguageProvider } from "./contexts/LanguageContext";
|
||||
import { SidebarProvider } from "./contexts/SidebarContext";
|
||||
|
||||
import Index from './pages/Index';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import InstanceMonitoring from './pages/InstanceMonitoring';
|
||||
import ContainerMonitoring from './pages/ContainerMonitoring';
|
||||
import ServiceDetail from './pages/ServiceDetail';
|
||||
import SslDomain from './pages/SslDomain';
|
||||
import ScheduleIncident from './pages/ScheduleIncident';
|
||||
import OperationalPage from './pages/OperationalPage';
|
||||
import RegionalMonitoring from './pages/RegionalMonitoring';
|
||||
import Settings from './pages/Settings';
|
||||
import Profile from './pages/Profile';
|
||||
import NotFound from './pages/NotFound';
|
||||
import PublicStatusPage from './pages/PublicStatusPage';
|
||||
import { ProtectedRoute } from './components/auth/ProtectedRoute';
|
||||
import ServerDetail from './pages/ServerDetail';
|
||||
import Index from "./pages/Index";
|
||||
import Login from "./pages/Login";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import InstanceMonitoring from "./pages/InstanceMonitoring";
|
||||
import ContainerMonitoring from "./pages/ContainerMonitoring";
|
||||
import ServiceDetail from "./pages/ServiceDetail";
|
||||
import SslDomain from "./pages/SslDomain";
|
||||
import ScheduleIncident from "./pages/ScheduleIncident";
|
||||
import OperationalPage from "./pages/OperationalPage";
|
||||
import RegionalMonitoring from "./pages/RegionalMonitoring";
|
||||
import Settings from "./pages/Settings";
|
||||
import Profile from "./pages/Profile";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import PublicStatusPage from "./pages/PublicStatusPage";
|
||||
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
|
||||
import ServerDetail from "./pages/ServerDetail";
|
||||
|
||||
function App() {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000
|
||||
}
|
||||
}
|
||||
}));
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
@@ -43,22 +46,113 @@ function App() {
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/public/:slug" element={<PublicStatusPage />} />
|
||||
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} />
|
||||
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
||||
<Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} />
|
||||
<Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/service/:id" element={<ProtectedRoute><ServiceDetail /></ProtectedRoute>} />
|
||||
<Route path="/ssl-domain" element={<ProtectedRoute><SslDomain /></ProtectedRoute>} />
|
||||
<Route path="/schedule-incident" element={<ProtectedRoute><ScheduleIncident /></ProtectedRoute>} />
|
||||
<Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} />
|
||||
<Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Index />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/instance-monitoring"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<InstanceMonitoring />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/server-detail/:serverId"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ServerDetail />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/container-monitoring"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ContainerMonitoring />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/container-monitoring/:serverId"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ContainerMonitoring />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/service/:id"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ServiceDetail />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/ssl-domain"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<SslDomain />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/schedule-incident"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ScheduleIncident />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/operational-page"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<OperationalPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/regional-monitoring"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<RegionalMonitoring />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Settings />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Profile />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
@@ -69,4 +163,4 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
@@ -7,6 +6,7 @@ import { StatusCards } from "./StatusCards";
|
||||
import { ServiceFilters } from "./ServiceFilters";
|
||||
import { ServicesTable } from "./ServicesTable";
|
||||
import { AddServiceDialog } from "@/components/services/AddServiceDialog";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface DashboardContentProps {
|
||||
services: Service[];
|
||||
@@ -14,16 +14,24 @@ interface DashboardContentProps {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
|
||||
export const DashboardContent = ({
|
||||
services,
|
||||
isLoading,
|
||||
error,
|
||||
}: DashboardContentProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [filter, setFilter] = useState<string>("all");
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
|
||||
|
||||
// Filter services based on search term and type filter
|
||||
const filteredServices = services.filter(service => {
|
||||
const matchesSearch = service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase();
|
||||
const filteredServices = services.filter((service) => {
|
||||
const matchesSearch =
|
||||
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(service.url &&
|
||||
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
const matchesFilter =
|
||||
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
|
||||
return matchesSearch && matchesFilter;
|
||||
});
|
||||
|
||||
@@ -31,40 +39,42 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
||||
<p>Error loading service data.</p>
|
||||
<Button onClick={() => window.location.reload()}>Retry</Button>
|
||||
<Button onClick={() => window.location.reload()}>{t("retry")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
||||
<div className="flex flex-col flex-1">
|
||||
<main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 ">
|
||||
<div className="flex flex-col flex-1 bg-white dark:bg-black p-4 rounded-xl ">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-foreground">Overview</h2>
|
||||
<Button
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{t("overview")}
|
||||
</h2>
|
||||
<Button
|
||||
className="text-primary-foreground"
|
||||
onClick={() => setIsAddDialogOpen(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" /> New Service
|
||||
<Plus className="w-4 h-4 mr-2" /> {t("newService")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<StatusCards services={services} />
|
||||
|
||||
<ServiceFilters
|
||||
|
||||
<ServiceFilters
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
servicesCount={filteredServices.length}
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex-1 flex flex-col pb-6">
|
||||
<ServicesTable services={filteredServices} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddServiceDialog
|
||||
<AddServiceDialog
|
||||
open={isAddDialogOpen}
|
||||
onOpenChange={setIsAddDialogOpen}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
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,
|
||||
CaseUpper,
|
||||
BookA,
|
||||
CaseSensitive,
|
||||
CaseLower,
|
||||
ALargeSmall,
|
||||
} 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 useDynamicFontSize from "@/contexts/FontSizeContext";
|
||||
|
||||
interface HeaderProps {
|
||||
currentUser: AuthUser | null;
|
||||
@@ -20,7 +46,7 @@ export const Header = ({
|
||||
currentUser,
|
||||
onLogout,
|
||||
sidebarCollapsed,
|
||||
toggleSidebar
|
||||
toggleSidebar,
|
||||
}: HeaderProps) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { language, setLanguage, t } = useLanguage();
|
||||
@@ -55,145 +81,270 @@ export const Header = ({
|
||||
}, [currentUser]);
|
||||
|
||||
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
|
||||
let avatarUrl = '';
|
||||
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/')) {
|
||||
if (currentUser.avatar.startsWith("/upload/profile/")) {
|
||||
avatarUrl = currentUser.avatar;
|
||||
} else {
|
||||
avatarUrl = currentUser.avatar;
|
||||
}
|
||||
console.log("Final avatar URL:", avatarUrl);
|
||||
}
|
||||
|
||||
const [fontSize, setFontSize] = useState(() => {
|
||||
const savedFontSize = localStorage.getItem("fontSize");
|
||||
return savedFontSize ? parseInt(savedFontSize, 10) : 14;
|
||||
});
|
||||
useDynamicFontSize(fontSize);
|
||||
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">
|
||||
<div
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`,
|
||||
backgroundSize: '20px 20px'
|
||||
<div
|
||||
className="w-full h-full dark:bg-black"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${
|
||||
theme === "dark" ? "#ffffff10" : "#00000010"
|
||||
} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${
|
||||
theme === "dark" ? "#ffffff10" : "#00000010"
|
||||
} 1px, transparent 1px)`,
|
||||
backgroundSize: "20px 20px",
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full backdrop-blur-[1px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Header Content */}
|
||||
<div className="flex items-center gap-4 z-10">
|
||||
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
|
||||
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
||||
<div
|
||||
className={` p-1 h-8 w-8 -ml-2 rounded bg-gray-600 ${
|
||||
!sidebarCollapsed ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src="/favicon_sidebar.ico"
|
||||
alt="CheckCle"
|
||||
className={`h-6 w-6 `}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
className="mr-2"
|
||||
>
|
||||
{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>
|
||||
<h1 className="font-medium text-xl">
|
||||
{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}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
{theme === "dark" ? (
|
||||
<Sun className="w-4 h-4" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
>
|
||||
<span className="sr-only">{t("language")}</span>
|
||||
<Globe className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage("en")}
|
||||
className={language === "en" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("english")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage("km")}
|
||||
className={language === "km" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("khmer")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage("de")}
|
||||
className={language === "de" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("Deutsch")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage("ko")}
|
||||
className={language === "ko" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("korean")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage("ja")}
|
||||
className={language === "ja" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("japanese")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage("zhcn")}
|
||||
className={language === "zhcn" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("simplifiedChinese")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
{/* Documentation */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
<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"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
|
||||
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"
|
||||
<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"
|
||||
<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">
|
||||
<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>
|
||||
|
||||
{/* Fond Size */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
>
|
||||
<span className="sr-only">{t("language")}</span>
|
||||
<BookA className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFontSize((prev) => prev + 1)}
|
||||
className={`cursor-pointer`}
|
||||
>
|
||||
<span className={`text-[15px] font-semibold`}> Aa +</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFontSize((prev) => prev - 1)}
|
||||
className={`cursor-pointer`}
|
||||
>
|
||||
<span className={`text-[12px] font-semibold`}> Aa -</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* User Profile Dropdown */}
|
||||
<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>}
|
||||
{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>
|
||||
</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={
|
||||
currentUser?.name ||
|
||||
currentUser?.email?.split("@")[0] ||
|
||||
"User"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className="bg-primary/20 text-primary">
|
||||
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<span className="font-medium">{currentUser?.name || 'User'}</span>
|
||||
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
|
||||
<span className="font-medium">
|
||||
{currentUser?.name || "User"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{currentUser?.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -206,7 +357,10 @@ export const Header = ({
|
||||
<span>{t("settings")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onLogout} className="text-red-500 focus:text-red-500">
|
||||
<DropdownMenuItem
|
||||
onClick={onLogout}
|
||||
className="text-red-500 focus:text-red-500"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>{t("logout")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@/components/ui/select";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServiceFiltersProps {
|
||||
filter: string;
|
||||
@@ -12,28 +18,31 @@ interface ServiceFiltersProps {
|
||||
servicesCount: number;
|
||||
}
|
||||
|
||||
export const ServiceFilters = ({
|
||||
filter,
|
||||
setFilter,
|
||||
searchTerm,
|
||||
export const ServiceFilters = ({
|
||||
filter,
|
||||
setFilter,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
servicesCount
|
||||
servicesCount,
|
||||
}: ServiceFiltersProps) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<div className="mb-6 flex justify-between items-center ">
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-xl font-semibold mr-2 text-foreground">Currently Monitoring</h3>
|
||||
<h3 className="text-xl font-semibold mr-2 text-foreground">
|
||||
{t("currentlyMonitoring")}
|
||||
</h3>
|
||||
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
|
||||
{servicesCount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<Select value={filter} onValueChange={setFilter}>
|
||||
<SelectTrigger className="w-40 bg-card border-border">
|
||||
<SelectValue placeholder="All Types" />
|
||||
<SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800">
|
||||
<SelectValue placeholder={t("allTypes")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="all">{t("allTypes")}</SelectItem>
|
||||
<SelectItem value="HTTP">HTTP</SelectItem>
|
||||
<SelectItem value="PING">PING</SelectItem>
|
||||
<SelectItem value="TCP">TCP</SelectItem>
|
||||
@@ -41,9 +50,9 @@ export const ServiceFilters = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-72 bg-card border-border"
|
||||
placeholder="Search"
|
||||
<Input
|
||||
className="w-72 bg-card border-border"
|
||||
placeholder={t("search")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { SidebarHeader } from "./sidebar/SidebarHeader";
|
||||
@@ -11,18 +10,25 @@ interface SidebarProps {
|
||||
|
||||
export const Sidebar = ({ collapsed }: SidebarProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
<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'}
|
||||
`}
|
||||
${
|
||||
theme === "dark"
|
||||
? "bg-black border-[#1e1e1e]"
|
||||
: "bg-sidebars border-sidebar-border"
|
||||
}
|
||||
border-r flex flex-col h-full
|
||||
${collapsed ? "w-16" : "w-64"}
|
||||
${collapsed ? "hidden" : ""}`}
|
||||
>
|
||||
<SidebarHeader collapsed={collapsed} />
|
||||
<MainNavigation collapsed={collapsed} />
|
||||
<SettingsPanel collapsed={collapsed} />
|
||||
<div className="items-center ml-2 p-2 h-30 border-t ">
|
||||
Version: latest
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react";
|
||||
import { Service } from "@/services/serviceService";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface StatusCardsProps {
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
export const StatusCards = ({ services }: StatusCardsProps) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Count services by status
|
||||
const upServices = services.filter(s => s.status === "up").length;
|
||||
const downServices = services.filter(s => s.status === "down").length;
|
||||
@@ -42,7 +45,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
||||
></div>
|
||||
</div>
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-white">UP SERVICES</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-white">{t("upServices")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between relative z-10">
|
||||
<span className="text-5xl font-bold text-white">{upServices}</span>
|
||||
@@ -74,7 +77,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
||||
></div>
|
||||
</div>
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-white">DOWN SERVICES</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-white">{t("downServices")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between relative z-10">
|
||||
<span className="text-5xl font-bold text-white">{downServices}</span>
|
||||
@@ -106,7 +109,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
||||
></div>
|
||||
</div>
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-white">PAUSED SERVICES</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-white">{t("pausedServices")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between relative z-10">
|
||||
<span className="text-5xl font-bold text-white">{pausedServices}</span>
|
||||
@@ -138,7 +141,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
|
||||
></div>
|
||||
</div>
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-white">WARNING SERVICES</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-white">{t("warningServices")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between relative z-10">
|
||||
<span className="text-5xl font-bold text-white">{warningServices}</span>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import { MenuItem } from "./MenuItem";
|
||||
import { mainMenuItems } from "./navigationData";
|
||||
@@ -7,9 +6,15 @@ interface MainNavigationProps {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
|
||||
export const MainNavigation: React.FC<MainNavigationProps> = ({
|
||||
collapsed,
|
||||
}) => {
|
||||
return (
|
||||
<nav className="my-2 mx-1 py-1 px-1">
|
||||
<nav
|
||||
className={`my-2 mx-1 py-1 px-1 dark:bg-black ${
|
||||
collapsed ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{mainMenuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
@@ -24,4 +29,4 @@ export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) =>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
@@ -22,7 +21,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
translationKey,
|
||||
color,
|
||||
hasNavigation,
|
||||
collapsed
|
||||
collapsed,
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
@@ -32,7 +31,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
if (hasNavigation && path) {
|
||||
navigate(path, { replace: false });
|
||||
}
|
||||
@@ -42,23 +41,29 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const mainIconSize = "h-6 w-6";
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={`
|
||||
${collapsed ? 'p-3' : 'p-2 pl-3'}
|
||||
${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'}`}
|
||||
${
|
||||
isActive
|
||||
? theme === "dark"
|
||||
? "bg-drak"
|
||||
: "bg-sidebar-accent "
|
||||
: `hover:${theme === "dark" ? "bg-gray-800" : "bg-sidebar-accent "}`
|
||||
}
|
||||
flex items-center
|
||||
${collapsed ? 'justify-center' : ''}
|
||||
${collapsed ? "justify-center" : ""}
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon className={`${mainIconSize} ${color}`} />
|
||||
{!collapsed && (
|
||||
<span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
|
||||
<span className="ml-2.5 font-medium text-foreground tracking-wide ">
|
||||
{t(translationKey)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Settings, ChevronDown } from "lucide-react";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { settingsMenuItems } from "./navigationData";
|
||||
|
||||
@@ -17,19 +20,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
||||
const { t } = useLanguage();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
|
||||
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
||||
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>(
|
||||
"general"
|
||||
);
|
||||
//const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
||||
const [settingsPanelOpen, setSettingsPanelOpen] = useState(() => {
|
||||
const savedValue = localStorage.getItem("settingsPanelOpen");
|
||||
// Check if the saved value exists and is "true"
|
||||
return savedValue === "true";
|
||||
});
|
||||
|
||||
// Update active settings item based on URL
|
||||
useEffect(() => {
|
||||
if (location.pathname === '/settings') {
|
||||
if (location.pathname === "/settings") {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const panel = params.get('panel');
|
||||
const panel = params.get("panel");
|
||||
if (panel) {
|
||||
setActiveSettingsItem(panel);
|
||||
}
|
||||
}
|
||||
}, [location]);
|
||||
localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString());
|
||||
}, [location, settingsPanelOpen]);
|
||||
|
||||
const handleSettingsItemClick = (item: string) => {
|
||||
setActiveSettingsItem(item);
|
||||
@@ -43,15 +54,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
||||
};
|
||||
|
||||
const getMenuItemClasses = (isActive: boolean) => {
|
||||
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
|
||||
return `p-2 ${
|
||||
isActive
|
||||
? theme === "dark"
|
||||
? "bg-[#1a1a1a]"
|
||||
: "bg-sidebar-accent"
|
||||
: `hover:${theme === "dark" ? "bg-[#1a1a1a]" : "bg-sidebar-accent"}`
|
||||
} rounded-lg flex items-center `;
|
||||
};
|
||||
|
||||
if (collapsed) {
|
||||
const mainIconSize = "h-6 w-6";
|
||||
return (
|
||||
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
|
||||
<div
|
||||
onClick={(e) => handleMenuItemClick('/settings', e)}
|
||||
<div
|
||||
className={`border-t ${
|
||||
theme === "dark"
|
||||
? "border-[#1e1e1e] bg-[#121212]"
|
||||
: "border-sidebar-border bg-sidebar"
|
||||
} p-4 flex justify-center ${collapsed ? "hidden" : ""}`}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => handleMenuItemClick("/settings", e)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Settings className={`${mainIconSize} text-purple-400`} />
|
||||
@@ -61,33 +84,59 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
|
||||
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
|
||||
<CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
|
||||
<div
|
||||
className={`flex-1 flex flex-col border-t ${
|
||||
theme === "dark"
|
||||
? "border-[#1e1e1e] bg-black"
|
||||
: "border-sidebar-border bg-sidebars"
|
||||
} p-4 ${collapsed ? "hidden" : ""}`}
|
||||
>
|
||||
<Collapsible
|
||||
open={settingsPanelOpen}
|
||||
onOpenChange={setSettingsPanelOpen}
|
||||
className="w-full flex flex-col flex-1"
|
||||
>
|
||||
<CollapsibleTrigger
|
||||
className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${
|
||||
theme === "dark" ? "hover:bg-[#1a1a1a]" : "hover:bg-sidebar-accent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium tracking-wide">{t("settingPanel")}</span>
|
||||
<span className="font-medium tracking-wide">
|
||||
{t("settingPanel")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform duration-200 ${
|
||||
settingsPanelOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}>
|
||||
|
||||
<CollapsibleContent
|
||||
className={`${
|
||||
theme === "dark" ? "bg-black" : "bg-sidebars"
|
||||
} flex-1 flex flex-col`}
|
||||
>
|
||||
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-2 pr-4">
|
||||
<div className="space-y-2 pr-4 cursor-pointer">
|
||||
{settingsMenuItems.map((item) => (
|
||||
<div
|
||||
<div
|
||||
key={item.id}
|
||||
className={getMenuItemClasses(activeSettingsItem === item.id)}
|
||||
className={getMenuItemClasses(
|
||||
activeSettingsItem === item.id
|
||||
)}
|
||||
onClick={(e) => {
|
||||
handleMenuItemClick(`/settings?panel=${item.id}`, e);
|
||||
handleSettingsItemClick(item.id);
|
||||
}}
|
||||
>
|
||||
<item.icon className="h-4 w-4 mr-2" />
|
||||
<span className="text-sm">{t(item.translationKey)}</span>
|
||||
<span className="text-base">{t(item.translationKey)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -97,4 +146,4 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from "react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
@@ -10,15 +9,17 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
||||
<div className="h-8 w-8 bg-gray-600 rounded flex items-center justify-center mr-2">
|
||||
<img
|
||||
src="/favicon_sidebar.ico"
|
||||
alt="CheckCle"
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
<div
|
||||
className={`p-[15.7px] ${
|
||||
theme === "dark"
|
||||
? "border-[#1e1e1e] bg-black"
|
||||
: "border-sidebar-border bg-white"
|
||||
} border-b flex items-center ${collapsed ? "justify-center" : ""}`}
|
||||
>
|
||||
<div className="h-8 w-8 rounded flex items-center justify-center mr-2 dark:bg-drak">
|
||||
<img src="/favicon_sidebar.ico" alt="CheckCle" className="h-7 w-6" />
|
||||
</div>
|
||||
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage';
|
||||
import { CreateOperationalPageDialog } from './CreateOperationalPageDialog';
|
||||
import { EditOperationalPageDialog } from './EditOperationalPageDialog';
|
||||
import { OperationalPageCard } from './OperationalPageCard';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
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 { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
useOperationalPages,
|
||||
useDeleteOperationalPage,
|
||||
} from "@/hooks/useOperationalPage";
|
||||
import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog";
|
||||
import { EditOperationalPageDialog } from "./EditOperationalPageDialog";
|
||||
import { OperationalPageCard } from "./OperationalPageCard";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
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 {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -18,17 +21,26 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
export const OperationalPageContent = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
|
||||
const {
|
||||
data: pages,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useOperationalPages();
|
||||
const deleteMutation = useDeleteOperationalPage();
|
||||
|
||||
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null);
|
||||
|
||||
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(
|
||||
null
|
||||
);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [pageToDelete, setPageToDelete] = useState<OperationalPageRecord | null>(null);
|
||||
const [pageToDelete, setPageToDelete] =
|
||||
useState<OperationalPageRecord | null>(null);
|
||||
|
||||
const handleEdit = (page: OperationalPageRecord) => {
|
||||
setEditingPage(page);
|
||||
@@ -37,10 +49,10 @@ export const OperationalPageContent = () => {
|
||||
|
||||
const handleView = (page: OperationalPageRecord) => {
|
||||
if (page.custom_domain) {
|
||||
window.open(`https://${page.custom_domain}`, '_blank');
|
||||
window.open(`https://${page.custom_domain}`, "_blank");
|
||||
} else {
|
||||
// Navigate to the public status page route using the correct format
|
||||
window.open(`/public/${page.slug}`, '_blank');
|
||||
window.open(`/public/${page.slug}`, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,7 +68,7 @@ export const OperationalPageContent = () => {
|
||||
setDeleteDialogOpen(false);
|
||||
setPageToDelete(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting page:', error);
|
||||
console.error("Error deleting page:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -68,7 +80,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">Failed to load operational pages</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Failed to load operational pages
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
There was an error loading your operational pages. Please try again.
|
||||
</p>
|
||||
@@ -82,16 +96,18 @@ export const OperationalPageContent = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="w-full container mx-auto px-4 py-8 ">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<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">
|
||||
Operational Pages
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your public status pages and monitor service health
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2 mt-4 sm:mt-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -99,7 +115,9 @@ export const OperationalPageContent = () => {
|
||||
onClick={() => refetch()}
|
||||
disabled={isRefetching}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
<CreateOperationalPageDialog />
|
||||
@@ -135,9 +153,12 @@ 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">
|
||||
No operational pages found
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Create your first operational page to start monitoring your services and communicate status to your users.
|
||||
Create your first operational page to start monitoring your
|
||||
services and communicate status to your users.
|
||||
</p>
|
||||
<CreateOperationalPageDialog />
|
||||
</CardContent>
|
||||
@@ -167,12 +188,12 @@ export const OperationalPageContent = () => {
|
||||
<span className="font-medium">Total Pages:</span> {pages.length}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Public Pages:</span>{' '}
|
||||
{pages.filter(p => p.is_public === 'true').length}
|
||||
<span className="font-medium">Public Pages:</span>{" "}
|
||||
{pages.filter((p) => p.is_public === "true").length}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Operational:</span>{' '}
|
||||
{pages.filter(p => p.status === 'operational').length}
|
||||
<span className="font-medium">Operational:</span>{" "}
|
||||
{pages.filter((p) => p.status === "operational").length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,7 +212,9 @@ export const OperationalPageContent = () => {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Operational Page</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.
|
||||
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.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -201,11 +224,11 @@ export const OperationalPageContent = () => {
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
{deleteMutation.isPending ? "Deleting..." : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
|
||||
import { regionalService } from "@/services/regionalService";
|
||||
import { RegionalService } from "@/types/regional.types";
|
||||
@@ -15,24 +20,29 @@ export const RegionalMonitoringContent = () => {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: regionalServices = [], isLoading, error } = useQuery({
|
||||
queryKey: ['regional-services'],
|
||||
const {
|
||||
data: regionalServices = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["regional-services"],
|
||||
queryFn: regionalService.getRegionalServices,
|
||||
refetchInterval: 30000, // Refetch every 30 seconds
|
||||
});
|
||||
|
||||
const handleAgentAdded = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
|
||||
toast({
|
||||
title: "Regional Agent Added",
|
||||
description: "The regional monitoring agent has been successfully configured.",
|
||||
description:
|
||||
"The regional monitoring agent has been successfully configured.",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAgent = async (id: string) => {
|
||||
try {
|
||||
await regionalService.deleteRegionalService(id);
|
||||
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
|
||||
toast({
|
||||
title: "Agent Removed",
|
||||
description: "The regional monitoring agent has been removed.",
|
||||
@@ -46,14 +56,18 @@ export const RegionalMonitoringContent = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onlineAgents = regionalServices.filter(agent => agent.connection === 'online').length;
|
||||
const onlineAgents = regionalServices.filter(
|
||||
(agent) => agent.connection === "online"
|
||||
).length;
|
||||
const totalAgents = regionalServices.length;
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<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">
|
||||
Regional Monitoring
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage distributed monitoring agents across different regions
|
||||
</p>
|
||||
@@ -85,23 +99,25 @@ export const RegionalMonitoringContent = () => {
|
||||
<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
|
||||
</p>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{onlineAgents}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Currently connected</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">
|
||||
Offline Agents
|
||||
</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
|
||||
</p>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{totalAgents - onlineAgents}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Disconnected agents</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -109,7 +125,7 @@ export const RegionalMonitoringContent = () => {
|
||||
{/* Agents List */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Regional Agents</h2>
|
||||
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
@@ -133,7 +149,8 @@ export const RegionalMonitoringContent = () => {
|
||||
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
|
||||
<p className="text-muted-foreground text-center mb-4">
|
||||
Get started by adding your first regional monitoring agent to extend your monitoring coverage.
|
||||
Get started by adding your first regional monitoring agent to
|
||||
extend your monitoring coverage.
|
||||
</p>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
@@ -144,9 +161,9 @@ export const RegionalMonitoringContent = () => {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{regionalServices.map((agent) => (
|
||||
<RegionalAgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
<RegionalAgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onDelete={() => handleDeleteAgent(agent.id)}
|
||||
/>
|
||||
))}
|
||||
@@ -161,4 +178,4 @@ export const RegionalMonitoringContent = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, CalendarClock, AlertCircle } from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
|
||||
import { IncidentManagementTab } from "./IncidentManagementTab";
|
||||
import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
|
||||
import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
|
||||
import { CreateMaintenanceDialog } from "./maintenance/CreateMaintenanceDialog";
|
||||
import { CreateIncidentDialog } from "./incident/CreateIncidentDialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
initMaintenanceNotifications,
|
||||
stopMaintenanceNotifications,
|
||||
} from "@/services/maintenance/maintenanceNotificationService";
|
||||
|
||||
export const ScheduleIncidentContent = () => {
|
||||
const { t } = useLanguage();
|
||||
const { toast } = useToast();
|
||||
const [activeTab, setActiveTab] = useState("maintenance");
|
||||
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
|
||||
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
|
||||
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] =
|
||||
useState(false);
|
||||
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] =
|
||||
useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
|
||||
|
||||
// Initialize maintenance notifications when the component mounts
|
||||
useEffect(() => {
|
||||
// console.log("Initializing maintenance notifications");
|
||||
// console.log("Initializing maintenance notifications");
|
||||
initMaintenanceNotifications();
|
||||
|
||||
|
||||
// Clean up when the component unmounts
|
||||
return () => {
|
||||
// console.log("Cleaning up maintenance notifications");
|
||||
// console.log("Cleaning up maintenance notifications");
|
||||
stopMaintenanceNotifications();
|
||||
};
|
||||
}, []);
|
||||
@@ -43,65 +47,67 @@ export const ScheduleIncidentContent = () => {
|
||||
const handleMaintenanceCreated = () => {
|
||||
// Refresh data by incrementing the refresh trigger
|
||||
const newTriggerValue = refreshTrigger + 1;
|
||||
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
|
||||
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
|
||||
setRefreshTrigger(newTriggerValue);
|
||||
|
||||
|
||||
// Show success toast
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('maintenanceCreatedSuccess'),
|
||||
title: t("success"),
|
||||
description: t("maintenanceCreatedSuccess"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleIncidentCreated = () => {
|
||||
// Refresh data by incrementing the refresh trigger
|
||||
const newTriggerValue = incidentRefreshTrigger + 1;
|
||||
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
|
||||
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
|
||||
setIncidentRefreshTrigger(newTriggerValue);
|
||||
|
||||
|
||||
// Show success toast
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('incidentCreatedSuccess'),
|
||||
title: t("success"),
|
||||
description: t("incidentCreatedSuccess"),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 ">
|
||||
<div className="flex flex-col gap-6 bg-white dark:bg-black p-3 h-full rounded-xl">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{t('scheduleIncidentManagement')}
|
||||
{t("scheduleIncidentManagement")}
|
||||
</h2>
|
||||
<Button
|
||||
<Button
|
||||
className="text-primary-foreground"
|
||||
onClick={handleCreateButtonClick}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{activeTab === "maintenance" ? t('createMaintenanceWindow') : t('createIncident')}
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{activeTab === "maintenance"
|
||||
? t("createMaintenanceWindow")
|
||||
: t("createIncident")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue="maintenance"
|
||||
|
||||
<Tabs
|
||||
defaultValue="maintenance"
|
||||
className="w-full"
|
||||
onValueChange={(value) => setActiveTab(value)}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||
<TabsTrigger value="maintenance">
|
||||
<CalendarClock className="w-4 h-4 mr-2" />
|
||||
{t('scheduledMaintenance')}
|
||||
{t("scheduledMaintenance")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="incidents">
|
||||
<AlertCircle className="w-4 h-4 mr-2" />
|
||||
{t('incidentManagement')}
|
||||
{t("incidentManagement")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
||||
<TabsContent value="maintenance" className="space-y-4">
|
||||
<ScheduledMaintenanceTab refreshTrigger={refreshTrigger} />
|
||||
</TabsContent>
|
||||
|
||||
|
||||
<TabsContent value="incidents" className="space-y-4">
|
||||
<IncidentManagementTab refreshTrigger={incidentRefreshTrigger} />
|
||||
</TabsContent>
|
||||
@@ -109,14 +115,14 @@ export const ScheduleIncidentContent = () => {
|
||||
</div>
|
||||
|
||||
{/* Maintenance creation dialog */}
|
||||
<CreateMaintenanceDialog
|
||||
<CreateMaintenanceDialog
|
||||
open={createMaintenanceDialogOpen}
|
||||
onOpenChange={setCreateMaintenanceDialogOpen}
|
||||
onMaintenanceCreated={handleMaintenanceCreated}
|
||||
/>
|
||||
|
||||
{/* Incident creation dialog */}
|
||||
<CreateIncidentDialog
|
||||
<CreateIncidentDialog
|
||||
open={createIncidentDialogOpen}
|
||||
onOpenChange={setCreateIncidentDialogOpen}
|
||||
onIncidentCreated={handleIncidentCreated}
|
||||
|
||||
+6
-5
@@ -25,19 +25,20 @@ export const IncidentDetailContent = ({
|
||||
onClose,
|
||||
assignedUser
|
||||
}: IncidentDetailContentProps) => {
|
||||
// Fetch assigned user details if one wasn't provided and there's an assigned_to field
|
||||
// Fetch assigned user details if none was provided; prefer server field
|
||||
const assigneeId = incident?.assigned_users || incident?.assigned_to;
|
||||
const { data: fetchedUser } = useQuery({
|
||||
queryKey: ['user', incident?.assigned_to],
|
||||
queryKey: ['user', assigneeId],
|
||||
queryFn: async () => {
|
||||
if (!incident?.assigned_to) return null;
|
||||
if (!assigneeId) return null;
|
||||
try {
|
||||
return await userService.getUser(incident.assigned_to);
|
||||
return await userService.getUser(assigneeId);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch assigned user:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
enabled: !!incident?.assigned_to && !assignedUser,
|
||||
enabled: !!assigneeId && !assignedUser,
|
||||
staleTime: 300000 // Cache for 5 minutes
|
||||
});
|
||||
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@ export const IncidentDetailDialog = ({
|
||||
onOpenChange,
|
||||
incident
|
||||
}: IncidentDetailDialogProps) => {
|
||||
// Fetch user data for assigned_to field
|
||||
// Fetch user data for assigned field (prefer assigned_users, fallback to assigned_to)
|
||||
const { data: users = [] } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
@@ -25,12 +25,12 @@ export const IncidentDetailDialog = ({
|
||||
return Array.isArray(usersList) ? usersList : [];
|
||||
},
|
||||
staleTime: 300000, // Cache for 5 minutes
|
||||
enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
|
||||
enabled: !!(incident?.assigned_users || incident?.assigned_to) && open // Only run query if there's an assigned value and dialog is open
|
||||
});
|
||||
|
||||
// Find the assigned user
|
||||
const assignedUser = incident?.assigned_to
|
||||
? users.find(user => user.id === incident.assigned_to)
|
||||
// Find the assigned user (prefer assigned_users, fallback to assigned_to)
|
||||
const assignedUser = (incident?.assigned_users || incident?.assigned_to)
|
||||
? users.find(user => user.id === (incident?.assigned_users || incident?.assigned_to))
|
||||
: null;
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
+5
-5
@@ -23,19 +23,19 @@ export {
|
||||
|
||||
// Legacy component - keeping this for backward compatibility with other imports
|
||||
export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
|
||||
// Fetch assigned user details if there's an assigned_to field
|
||||
// Fetch assigned user details if there's an assigned field (prefer assigned_users, fallback to assigned_to)
|
||||
const { data: assignedUser } = useQuery({
|
||||
queryKey: ['user', incident?.assigned_to],
|
||||
queryKey: ['user', incident?.assigned_users || incident?.assigned_to],
|
||||
queryFn: async () => {
|
||||
if (!incident?.assigned_to) return null;
|
||||
if (!(incident?.assigned_users || incident?.assigned_to)) return null;
|
||||
try {
|
||||
return await userService.getUser(incident.assigned_to);
|
||||
return await userService.getUser(incident?.assigned_users || incident?.assigned_to);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch assigned user:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
enabled: !!incident?.assigned_to,
|
||||
enabled: !!(incident?.assigned_users || incident?.assigned_to),
|
||||
staleTime: 300000 // Cache for 5 minutes
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ export const AssignmentSection: React.FC<AssignmentSectionProps> = ({ incident,
|
||||
</Avatar>
|
||||
<span>{assignedUser.full_name || assignedUser.username}</span>
|
||||
</div>
|
||||
) : incident.assigned_to ? (
|
||||
<span>{incident.assigned_to}</span>
|
||||
) : (incident.assigned_users || incident.assigned_to) ? (
|
||||
<span>{incident.assigned_users || incident.assigned_to}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -50,8 +50,8 @@ export const BasicInfoSection: React.FC<BasicInfoSectionProps> = ({ incident, as
|
||||
</Avatar>
|
||||
<span>{assignedUser.full_name || assignedUser.username}</span>
|
||||
</div>
|
||||
) : incident.assigned_to ? (
|
||||
<span>{incident.assigned_to}</span>
|
||||
) : (incident.assigned_users || incident.assigned_to) ? (
|
||||
<span>{incident.assigned_users || incident.assigned_to}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
||||
)}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const IncidentBasicFields: React.FC = () => {
|
||||
const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
|
||||
|
||||
// Function to get user initials from name
|
||||
const getUserInitials = (user: any): string => {
|
||||
const getUserInitials = (user: { full_name?: string; username: string }): string => {
|
||||
if (user.full_name) {
|
||||
const nameParts = user.full_name.split(' ');
|
||||
if (nameParts.length > 1) {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const useIncidentEditForm = (
|
||||
impact: (incident.impact?.toLowerCase() || 'minor') as any,
|
||||
priority: (incident.priority?.toLowerCase() || 'medium') as any,
|
||||
service_id: incident.service_id || '',
|
||||
assigned_to: incident.assigned_to || '',
|
||||
assigned_to: incident.assigned_users || incident.assigned_to || '',
|
||||
root_cause: incident.root_cause || '',
|
||||
resolution_steps: incident.resolution_steps || '',
|
||||
lessons_learned: incident.lessons_learned || '',
|
||||
@@ -45,11 +45,14 @@ export const useIncidentEditForm = (
|
||||
impact: data.impact,
|
||||
priority: data.priority,
|
||||
service_id: data.service_id,
|
||||
assigned_to: data.assigned_to, // This is the user ID from the form
|
||||
...(data.assigned_to
|
||||
? { assigned_to: data.assigned_to, assigned_users: data.assigned_to }
|
||||
: {}),
|
||||
root_cause: data.root_cause,
|
||||
resolution_steps: data.resolution_steps,
|
||||
lessons_learned: data.lessons_learned,
|
||||
});
|
||||
|
||||
|
||||
toast({
|
||||
title: t('incidentUpdated'),
|
||||
|
||||
@@ -57,6 +57,7 @@ export const useIncidentForm = (onSuccess: () => void, onClose: () => void) => {
|
||||
priority: data.priority,
|
||||
service_id: data.service_id,
|
||||
assigned_to: data.assigned_to,
|
||||
assigned_users: data.assigned_to, // map to server field
|
||||
root_cause: data.root_cause,
|
||||
resolution_steps: data.resolution_steps,
|
||||
lessons_learned: data.lessons_learned,
|
||||
|
||||
@@ -86,7 +86,7 @@ export const IncidentTableRow = memo(({
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<AssignedUserCell userId={localItem.assigned_to} />
|
||||
<AssignedUserCell userId={localItem.assigned_users || localItem.assigned_to} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex justify-end items-center space-x-2">
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
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 { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
RefreshCw,
|
||||
Search,
|
||||
Eye,
|
||||
Activity,
|
||||
MoreHorizontal,
|
||||
Pause,
|
||||
Play,
|
||||
Edit,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Server } from "@/types/server.types";
|
||||
import { ServerStatusBadge } from "./ServerStatusBadge";
|
||||
import { OSTypeIcon } from "./OSTypeIcon";
|
||||
@@ -23,7 +55,11 @@ interface ServerTableProps {
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
|
||||
export const ServerTable = ({
|
||||
servers,
|
||||
isLoading,
|
||||
onRefresh,
|
||||
}: ServerTableProps) => {
|
||||
const { theme } = useTheme();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -34,10 +70,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const filteredServers = servers.filter(server =>
|
||||
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const filteredServers = servers.filter(
|
||||
(server) =>
|
||||
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleViewDetails = (serverId: string) => {
|
||||
@@ -51,41 +88,44 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
const handlePauseResume = async (server: Server) => {
|
||||
const serverId = server.id;
|
||||
const isPaused = server.status === "paused";
|
||||
|
||||
|
||||
if (pausingServers.has(serverId)) {
|
||||
return; // Already processing this server
|
||||
}
|
||||
|
||||
try {
|
||||
setPausingServers(prev => new Set(prev).add(serverId));
|
||||
|
||||
setPausingServers((prev) => new Set(prev).add(serverId));
|
||||
|
||||
// Only update the status field, preserving all other server configuration
|
||||
const updateData = {
|
||||
status: isPaused ? "up" : "paused",
|
||||
last_checked: new Date().toISOString()
|
||||
last_checked: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await pb.collection('servers').update(serverId, updateData);
|
||||
|
||||
|
||||
await pb.collection("servers").update(serverId, updateData);
|
||||
|
||||
toast({
|
||||
title: isPaused ? "Server resumed" : "Server paused",
|
||||
description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
|
||||
description: `Monitoring ${isPaused ? "resumed" : "paused"} for ${
|
||||
server.name
|
||||
}`,
|
||||
});
|
||||
|
||||
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
|
||||
|
||||
|
||||
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
|
||||
|
||||
// Refresh the server list to show updated status
|
||||
onRefresh();
|
||||
|
||||
} catch (error) {
|
||||
// console.error('Error updating server status:', error);
|
||||
// console.error('Error updating server status:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
|
||||
description: `Failed to ${
|
||||
isPaused ? "resume" : "pause"
|
||||
} server monitoring. Please try again.`,
|
||||
});
|
||||
} finally {
|
||||
setPausingServers(prev => {
|
||||
setPausingServers((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(serverId);
|
||||
return newSet;
|
||||
@@ -108,24 +148,23 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
|
||||
|
||||
// Delete the server from the database
|
||||
await pb.collection('servers').delete(selectedServer.id);
|
||||
|
||||
await pb.collection("servers").delete(selectedServer.id);
|
||||
|
||||
toast({
|
||||
title: "Server deleted",
|
||||
description: `${selectedServer.name} has been deleted successfully.`,
|
||||
});
|
||||
|
||||
|
||||
// Refresh the server list
|
||||
onRefresh();
|
||||
|
||||
|
||||
// Close the dialog
|
||||
setDeleteDialogOpen(false);
|
||||
setSelectedServer(null);
|
||||
|
||||
} catch (error) {
|
||||
// console.error('Error deleting server:', error);
|
||||
// console.error('Error deleting server:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
@@ -136,65 +175,65 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
}
|
||||
};
|
||||
|
||||
const CustomProgressBar = ({
|
||||
value,
|
||||
label,
|
||||
subtitle,
|
||||
type
|
||||
}: {
|
||||
value: number;
|
||||
label: string;
|
||||
subtitle: string;
|
||||
type: 'cpu' | 'memory' | 'disk'
|
||||
const CustomProgressBar = ({
|
||||
value,
|
||||
label,
|
||||
subtitle,
|
||||
type,
|
||||
}: {
|
||||
value: number;
|
||||
label: string;
|
||||
subtitle: string;
|
||||
type: "cpu" | "memory" | "disk";
|
||||
}) => {
|
||||
const getGradientColors = (type: string, value: number) => {
|
||||
if (type === 'cpu') {
|
||||
if (value > 90) return 'from-red-500 to-red-600';
|
||||
if (value > 75) return 'from-orange-500 to-orange-600';
|
||||
if (value > 60) return 'from-yellow-500 to-yellow-600';
|
||||
return 'from-green-500 to-green-600';
|
||||
if (type === "cpu") {
|
||||
if (value > 90) return "from-red-500 to-red-600";
|
||||
if (value > 75) return "from-orange-500 to-orange-600";
|
||||
if (value > 60) return "from-yellow-500 to-yellow-600";
|
||||
return "from-green-500 to-green-600";
|
||||
}
|
||||
if (type === 'memory') {
|
||||
if (value > 90) return 'from-red-500 to-red-600';
|
||||
if (value > 75) return 'from-yellow-500 to-yellow-600';
|
||||
return 'from-blue-500 to-blue-600';
|
||||
if (type === "memory") {
|
||||
if (value > 90) return "from-red-500 to-red-600";
|
||||
if (value > 75) return "from-yellow-500 to-yellow-600";
|
||||
return "from-blue-500 to-blue-600";
|
||||
}
|
||||
if (type === 'disk') {
|
||||
if (value > 95) return 'from-red-500 to-red-600';
|
||||
if (value > 85) return 'from-yellow-500 to-yellow-600';
|
||||
return 'from-orange-500 to-orange-600';
|
||||
if (type === "disk") {
|
||||
if (value > 95) return "from-red-500 to-red-600";
|
||||
if (value > 85) return "from-yellow-500 to-yellow-600";
|
||||
return "from-orange-500 to-orange-600";
|
||||
}
|
||||
return 'from-gray-500 to-gray-600';
|
||||
return "from-gray-500 to-gray-600";
|
||||
};
|
||||
|
||||
const getTextColor = (value: number) => {
|
||||
if (value > 90) return 'text-red-600 dark:text-red-400';
|
||||
if (value > 75) return 'text-orange-600 dark:text-orange-400';
|
||||
if (value > 60) return 'text-yellow-600 dark:text-yellow-400';
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
if (value > 90) return "text-red-600 dark:text-red-400";
|
||||
if (value > 75) return "text-orange-600 dark:text-orange-400";
|
||||
if (value > 60) return "text-yellow-600 dark:text-yellow-400";
|
||||
return "text-green-600 dark:text-green-400";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 min-w-[120px]">
|
||||
<div className="space-y-2 min-w-[120px] -mt-20">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className={`text-sm font-semibold ${getTextColor(value)}`}>
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{subtitle}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{subtitle} </span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner">
|
||||
<div
|
||||
className={`h-full bg-gradient-to-r ${getGradientColors(type, value)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
|
||||
<div
|
||||
className={`h-full bg-gradient-to-r ${getGradientColors(
|
||||
type,
|
||||
value
|
||||
)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
|
||||
style={{ width: `${Math.min(value, 100)}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse opacity-60" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -202,7 +241,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="flex-1 flex flex-col">
|
||||
<Card className="flex-1 flex flex-col ">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<CardTitle>Servers</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -244,33 +283,113 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<p className="text-muted-foreground">No servers found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
|
||||
<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>
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredServers.map((server) => {
|
||||
const cpuUsage = server.cpu_usage || 0;
|
||||
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
|
||||
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
|
||||
const memoryUsage =
|
||||
server.ram_total > 0
|
||||
? (server.ram_used / server.ram_total) * 100
|
||||
: 0;
|
||||
const diskUsage =
|
||||
server.disk_total > 0
|
||||
? (server.disk_used / server.disk_total) * 100
|
||||
: 0;
|
||||
const isPaused = server.status === "paused";
|
||||
const isProcessing = pausingServers.has(server.id);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={server.id}
|
||||
<TableRow
|
||||
key={server.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => handleViewDetails(server.id)}
|
||||
>
|
||||
@@ -285,7 +404,10 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<OSTypeIcon osType={server.os_type} />
|
||||
<span className="text-sm truncate" title={server.os_type}>
|
||||
<span
|
||||
className="text-sm truncate"
|
||||
title={server.os_type}
|
||||
>
|
||||
{server.os_type}
|
||||
</span>
|
||||
</div>
|
||||
@@ -307,7 +429,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<CustomProgressBar
|
||||
value={memoryUsage}
|
||||
label={`${memoryUsage.toFixed(1)}%`}
|
||||
subtitle={serverService.formatBytes(server.ram_total)}
|
||||
subtitle={serverService.formatBytes(
|
||||
server.ram_total
|
||||
)}
|
||||
type="memory"
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -315,12 +439,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<CustomProgressBar
|
||||
value={diskUsage}
|
||||
label={`${diskUsage.toFixed(1)}%`}
|
||||
subtitle={serverService.formatBytes(server.disk_total)}
|
||||
subtitle={serverService.formatBytes(
|
||||
server.disk_total
|
||||
)}
|
||||
type="disk"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm truncate" title={server.uptime}>
|
||||
<div
|
||||
className="text-sm truncate"
|
||||
title={server.uptime}
|
||||
>
|
||||
{server.uptime}
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -329,10 +458,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
{new Date(server.last_checked).toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<TableCell
|
||||
className="text-right"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
{isProcessing ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
@@ -341,20 +477,36 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
|
||||
<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
|
||||
</DropdownMenuItem>
|
||||
{server.docker === 'true' && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
|
||||
{server.docker === "true" && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleViewContainers(server.id);
|
||||
}}
|
||||
>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Container Monitoring
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePauseResume(server);
|
||||
}}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isPaused ? (
|
||||
@@ -370,12 +522,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(server);
|
||||
}}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(server);
|
||||
}}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
@@ -406,19 +566,19 @@ 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>
|
||||
Are you sure you want to delete this server?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete{' '}
|
||||
This action cannot be undone. This will permanently delete{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
{selectedServer?.name}
|
||||
</span>{' '}
|
||||
</span>{" "}
|
||||
and all of its monitoring data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { ServiceForm } from "./ServiceForm";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface AddServiceDialogProps {
|
||||
open: boolean;
|
||||
@@ -16,6 +17,7 @@ interface AddServiceDialogProps {
|
||||
}
|
||||
|
||||
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
|
||||
const { t } = useLanguage();
|
||||
const queryClient = useQueryClient();
|
||||
const handleSuccess = async () => {
|
||||
// Immediately invalidate and refetch services data
|
||||
@@ -32,9 +34,9 @@ export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps)
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Create New Service</DialogTitle>
|
||||
<DialogTitle className="text-xl">{t("createNewService")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill in the details to create a new service to monitor.
|
||||
{t("createNewServiceDesc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from "react";
|
||||
import { Clock, TimerOff } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface LastCheckedTimeProps {
|
||||
lastCheckedTime: string;
|
||||
@@ -10,6 +11,7 @@ interface LastCheckedTimeProps {
|
||||
}
|
||||
|
||||
export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheckedTimeProps) => {
|
||||
const { t } = useLanguage();
|
||||
// Format the time without seconds to display a static time
|
||||
const formatTimeWithoutSeconds = (timeString: string) => {
|
||||
try {
|
||||
@@ -55,7 +57,7 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
|
||||
<Clock className="h-4 w-4" />
|
||||
)}
|
||||
<span>
|
||||
{isPaused ? "Paused at " : ""}
|
||||
{isPaused ? t("pausedAt") : ""}
|
||||
{formattedTime}
|
||||
</span>
|
||||
</div>
|
||||
@@ -66,14 +68,14 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
|
||||
>
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="font-medium">
|
||||
{isPaused ? "Monitoring Paused" : "Last Check Details"}
|
||||
{isPaused ? t("monitoringPaused") : t("lastCheckDetails")}
|
||||
</div>
|
||||
<div>
|
||||
{isPaused ? "No automatic checks" : `Checked at ${formattedTime}`}
|
||||
{isPaused ? t("noAutomaticChecks") : t("checkedAt") + `${formattedTime}`}
|
||||
</div>
|
||||
{interval && !isPaused && (
|
||||
<div>
|
||||
Check interval: {formatInterval(interval)}
|
||||
{t("checkInterval")}: {formatInterval(interval)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-gray-400 text-[10px]">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
export function LoadingState() {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background text-foreground">
|
||||
<div className="flex flex-col items-center text-center py-8 px-4 rounded-lg shadow-lg bg-card animate-fade-in">
|
||||
@@ -7,8 +9,8 @@ export function LoadingState() {
|
||||
<div className="absolute w-12 h-12 rounded-full border-4 border-primary/20"></div>
|
||||
<div className="w-12 h-12 rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-1">Loading server data</h3>
|
||||
<p className="text-muted-foreground">Please wait while we retrieve your information...</p>
|
||||
<h3 className="text-xl font-medium mb-1">{t("loadingServerData")}</h3>
|
||||
<p className="text-muted-foreground">{t("retrievingYourInformation")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Service } from "@/types/service.types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServiceEditDialogProps {
|
||||
open: boolean;
|
||||
@@ -19,6 +20,7 @@ interface ServiceEditDialogProps {
|
||||
}
|
||||
|
||||
export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) {
|
||||
const { t } = useLanguage();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@@ -53,9 +55,9 @@ export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDi
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Edit Service</DialogTitle>
|
||||
<DialogTitle className="text-xl">{t("editService")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the details of your monitored service.
|
||||
{t("editServiceDesc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{open && service && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Service, UptimeData } from "@/types/service.types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useMemo } from "react";
|
||||
import { formatDistanceStrict } from "date-fns";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceStatsCardsProps {
|
||||
service: Service;
|
||||
@@ -11,6 +12,8 @@ interface ServiceStatsCardsProps {
|
||||
}
|
||||
|
||||
export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) {
|
||||
|
||||
const { t } = useLanguage();
|
||||
// Calculate uptime percentage
|
||||
const calculateUptimePercentage = () => {
|
||||
if (uptimeData.length === 0) return 100;
|
||||
@@ -109,41 +112,46 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Card className="border-blue-400 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/50 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">Response Time</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">{t('responseTime')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-700 dark:text-white">
|
||||
{service.responseTime}ms
|
||||
</div>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Last checked at {service.lastChecked}</p>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('lastCheckedAt').replace('{datetime}', service.lastChecked)}</p>
|
||||
{avgResponseTime > 0 && avgResponseTime !== service.responseTime && (
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Avg: {avgResponseTime}ms (last {upChecks.length} up checks)</p>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('avg')}: {avgResponseTime}ms ({t('lastUpChecksCount').replace('{count}', String(upChecks.length))})</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-green-400 dark:border-green-700 bg-green-50 dark:bg-green-900/50 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">Uptime</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">{t('uptime')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-700 dark:text-white">
|
||||
{uptimePercentage}%
|
||||
</div>
|
||||
<p className="text-xs text-green-600 dark:text-green-200 mt-1">Based on last {uptimeData.length} checks</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-200 mt-1">{t('basedOnlastChecksCount').replace('{count}', String(uptimeData.length))}</p>
|
||||
<div className="flex flex-col space-y-1 mt-2">
|
||||
<div className="flex items-center text-xs text-green-600 dark:text-green-200">
|
||||
<ArrowUp className="h-3 w-3 mr-1 text-green-600 dark:text-green-400" />
|
||||
<span>Total uptime: {uptimeStats.totalUptimeFormatted}</span>
|
||||
<span>{t("totalUptime")}: {uptimeStats.totalUptimeFormatted}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-red-600 dark:text-red-200">
|
||||
<ArrowDown className="h-3 w-3 mr-1 text-red-600 dark:text-red-400" />
|
||||
<span>Total downtime: {uptimeStats.totalDowntimeFormatted}</span>
|
||||
<span>{t("totalDowntime")}: {uptimeStats.totalDowntimeFormatted}</span>
|
||||
</div>
|
||||
{service.status !== "paused" && (
|
||||
<div className="flex items-center text-xs font-medium mt-1">
|
||||
<span className={service.status === "up" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
|
||||
{service.status === "up" ? "Up" : "Down"} for {uptimeStats.currentStatusDuration}
|
||||
{
|
||||
service.status === "up" ?
|
||||
t('upStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
|
||||
:
|
||||
t('downStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -153,16 +161,16 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
|
||||
|
||||
<Card className="border-purple-400 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/50 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">Monitoring Settings</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">{t('monitoringSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200">
|
||||
<Clock className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
||||
<span>Checked every {service.interval} seconds</span>
|
||||
<span>{t('monitoringSettingsInterval').replace('{interval}', String(service.interval))}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200 mt-1">
|
||||
<Server className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
||||
<span>{service.type} monitoring</span>
|
||||
<span>{service.type} {t('monitoringSettingsType')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -80,9 +80,9 @@ export function ServicesPagination({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{totalItems > 0
|
||||
? `${startItem}-${endItem} of ${totalItems} ${t("services") || "services"}`
|
||||
: `0 ${t("services") || "services"}`
|
||||
{totalItems > 0
|
||||
? t("servicesPagination", {"startItem": startItem, "endItem": endItem, "totalItems": totalItems})
|
||||
: t("servicesPaginationNoService")
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
import { UptimeData } from '@/types/service.types';
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface UptimeBarProps {
|
||||
uptime: number;
|
||||
@@ -15,6 +16,7 @@ interface UptimeBarProps {
|
||||
}
|
||||
|
||||
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
|
||||
const { t } = useLanguage();
|
||||
// Calculate date range for last 20 checks with much more aggressive caching
|
||||
const endDate = useMemo(() => new Date(), []);
|
||||
const startDate = useMemo(() => {
|
||||
@@ -23,19 +25,21 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
|
||||
return start;
|
||||
}, [endDate, interval]);
|
||||
|
||||
// Fetch uptime data with very aggressive caching to reduce API calls
|
||||
// Fetch uptime data with immediate display and background updates
|
||||
const { data: uptimeData = [] } = useQuery({
|
||||
queryKey: ['uptime-bar', serviceId, serviceType],
|
||||
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
|
||||
enabled: !!serviceId,
|
||||
staleTime: 30000, // Data is fresh for 30 seconds
|
||||
gcTime: 600000, // Keep in cache for 10 minutes
|
||||
refetchInterval: 60000, // 1 minute polling
|
||||
staleTime: 300000, // Data stays fresh for 5 minutes - display immediately from cache
|
||||
gcTime: 900000, // Keep in cache for 15 minutes
|
||||
refetchInterval: 300000, // Reduced to 5 minute polling
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
retry: 1, // Reduce retries for faster failure
|
||||
retryDelay: 1000, // Faster retry delay
|
||||
refetchIntervalInBackground: true, // Allow background updates
|
||||
placeholderData: (previousData) => previousData, // Keep showing previous data while refetching
|
||||
});
|
||||
|
||||
// Memoize the uptime calculation to prevent unnecessary recalculations
|
||||
@@ -84,7 +88,7 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Last 20 checks
|
||||
{t('last20Checks')}
|
||||
</span>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -3,22 +3,24 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/comp
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { ServiceFormData } from "./types";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceBasicFieldsProps {
|
||||
form: UseFormReturn<ServiceFormData>;
|
||||
}
|
||||
|
||||
export function ServiceBasicFields({ form }: ServiceBasicFieldsProps) {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Service Name</FormLabel>
|
||||
<FormLabel>{t('serviceName')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a descriptive name for your service"
|
||||
placeholder={t('serviceNameDesc')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -6,12 +6,14 @@ import { UseFormReturn } from "react-hook-form";
|
||||
import { ServiceFormData } from "./types";
|
||||
import { ServiceUrlField } from "./ServiceUrlField";
|
||||
import { useState } from "react";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceConfigFieldsProps {
|
||||
form: UseFormReturn<ServiceFormData>;
|
||||
}
|
||||
|
||||
export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||
const { t } = useLanguage();
|
||||
const [isCustomInterval, setIsCustomInterval] = useState(false);
|
||||
const intervalValue = form.watch("interval");
|
||||
|
||||
@@ -35,7 +37,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||
name="interval"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Check Interval</FormLabel>
|
||||
<FormLabel>{t("checkInterval")}</FormLabel>
|
||||
{!isCustomInterval ? (
|
||||
<FormControl>
|
||||
<Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}>
|
||||
@@ -43,13 +45,13 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">30 seconds</SelectItem>
|
||||
<SelectItem value="60">1 minute</SelectItem>
|
||||
<SelectItem value="300">5 minutes</SelectItem>
|
||||
<SelectItem value="900">15 minutes</SelectItem>
|
||||
<SelectItem value="1800">30 minutes</SelectItem>
|
||||
<SelectItem value="3600">1 hour</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
<SelectItem value="30">30 {t("seconds")}</SelectItem>
|
||||
<SelectItem value="60">1 {t("minute")}</SelectItem>
|
||||
<SelectItem value="300">5 {t("minutes")}</SelectItem>
|
||||
<SelectItem value="900">15 {t("minutes")}</SelectItem>
|
||||
<SelectItem value="1800">30 {t("minutes")}</SelectItem>
|
||||
<SelectItem value="3600">1 {t("hour")}</SelectItem>
|
||||
<SelectItem value="custom">{t("custom")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -58,7 +60,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter interval in seconds"
|
||||
placeholder={t("checkIntervalPlaceholder")}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
min="10"
|
||||
@@ -72,14 +74,14 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Back to presets
|
||||
{t("backToPresets")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<FormDescription className="text-xs">
|
||||
{isCustomInterval
|
||||
? "Enter custom interval in seconds (minimum 10 seconds)"
|
||||
: "How often to check the service status"
|
||||
? t("checkIntervalDescCustom")
|
||||
: t("checkIntervalDesc")
|
||||
}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -92,22 +94,22 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
|
||||
name="retries"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Retry Attempts</FormLabel>
|
||||
<FormLabel>{t("retryAttempts")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<Select onValueChange={field.onChange} value={field.value ?? "3"}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</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">1 {t("attempt")}</SelectItem>
|
||||
<SelectItem value="2">2 {t("attempts")}</SelectItem>
|
||||
<SelectItem value="3">3 {t("attempts")}</SelectItem>
|
||||
<SelectItem value="5">5 {t("attempts")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription className="text-xs">
|
||||
Number of retry attempts before marking as down
|
||||
{t("retryAttemptsDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { serviceSchema, ServiceFormData } from "./types";
|
||||
import { useServiceSchema, ServiceFormData } from "./types";
|
||||
import { ServiceBasicFields } from "./ServiceBasicFields";
|
||||
import { ServiceTypeField } from "./ServiceTypeField";
|
||||
import { ServiceConfigFields } from "./ServiceConfigFields";
|
||||
@@ -14,6 +14,7 @@ import { Service } from "@/types/service.types";
|
||||
import { ServiceRegionalFields } from "./ServiceRegionalFields";
|
||||
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceFormProps {
|
||||
onSuccess: () => void;
|
||||
@@ -30,10 +31,13 @@ export function ServiceForm({
|
||||
isEdit = false,
|
||||
onSubmitStart
|
||||
}: ServiceFormProps) {
|
||||
const { t } = useLanguage();
|
||||
const { toast } = useToast();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const serviceSchema = useServiceSchema();
|
||||
|
||||
// Initialize form with default values
|
||||
const form = useForm<ServiceFormData>({
|
||||
resolver: zodResolver(serviceSchema),
|
||||
@@ -124,23 +128,23 @@ export function ServiceForm({
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('basicInformation')}</h3>
|
||||
<ServiceBasicFields form={form} />
|
||||
<ServiceTypeField form={form} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('configuration')}</h3>
|
||||
<ServiceConfigFields form={form} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('regionalMonitoring')}</h3>
|
||||
<ServiceRegionalFields form={form} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('notifications')}</h3>
|
||||
<ServiceNotificationFields form={form} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +152,7 @@ export function ServiceForm({
|
||||
<ServiceFormActions
|
||||
isSubmitting={isSubmitting}
|
||||
onCancel={onCancel}
|
||||
submitLabel={isEdit ? "Update Service" : "Create Service"}
|
||||
submitLabel={isEdit ? t("updateService") : t("createService")}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { MouseEvent } from "react";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceFormActionsProps {
|
||||
isSubmitting: boolean;
|
||||
@@ -14,6 +15,8 @@ export function ServiceFormActions({
|
||||
onCancel,
|
||||
submitLabel = "Create Service"
|
||||
}: ServiceFormActionsProps) {
|
||||
|
||||
const {t} = useLanguage();
|
||||
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!isSubmitting) {
|
||||
@@ -29,7 +32,7 @@ export function ServiceFormActions({
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -38,7 +41,7 @@ export function ServiceFormActions({
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
{t("processing")}...
|
||||
</>
|
||||
) : (
|
||||
submitLabel
|
||||
|
||||
@@ -9,12 +9,14 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceNotificationFieldsProps {
|
||||
form: UseFormReturn<ServiceFormData>;
|
||||
}
|
||||
|
||||
export function ServiceNotificationFields({ form }: ServiceNotificationFieldsProps) {
|
||||
const { t } = useLanguage();
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
|
||||
// Get the current form values for debugging
|
||||
@@ -88,10 +90,10 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Enable Notifications
|
||||
{t("enableNotifications")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Enable or disable notifications for this service
|
||||
{t("enableNotificationsDesc")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -115,11 +117,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
name="notificationChannels"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notification Channels</FormLabel>
|
||||
<FormLabel>{t("notificationChannels")}</FormLabel>
|
||||
<FormDescription>
|
||||
{notificationStatus === "enabled"
|
||||
? "Select notification channels for this service"
|
||||
: "Enable notifications first to select channels"}
|
||||
? t("notificationChannelsEnabledDesc")
|
||||
: t("notificationChannelsDesc")}
|
||||
</FormDescription>
|
||||
|
||||
{/* Display selected channels as badges */}
|
||||
@@ -144,7 +146,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
value="" // Always reset to empty after selection
|
||||
>
|
||||
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||
<SelectValue placeholder="Add a notification channel" />
|
||||
<SelectValue placeholder={t("notificationChannelsPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{alertConfigs
|
||||
@@ -169,7 +171,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Alert Template</FormLabel>
|
||||
<FormLabel>{t("alertTemplate")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
@@ -179,7 +181,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
|
||||
>
|
||||
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||
<SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
|
||||
<SelectValue placeholder={isLoadingTemplates ? t("alertTemplateLoading") : t("alertTemplatePlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serviceTemplates?.map((template) => (
|
||||
@@ -192,8 +194,8 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{notificationStatus === "enabled"
|
||||
? "Choose a template for alert messages"
|
||||
: "Enable notifications first to select template"}
|
||||
? t("alertTemplateEnabledDesc")
|
||||
: t("alertTemplateDesc")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
@@ -9,12 +9,14 @@ import { regionalService } from "@/services/regionalService";
|
||||
import { MapPin, Loader2, X, Plus } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceRegionalFieldsProps {
|
||||
form: UseFormReturn<ServiceFormData>;
|
||||
}
|
||||
|
||||
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
const { t } = useLanguage();
|
||||
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
|
||||
const currentRegionalAgents = form.watch("regionalAgents") || [];
|
||||
|
||||
@@ -79,10 +81,10 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base font-medium flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4" />
|
||||
Regional Monitoring
|
||||
{t("regionalMonitoring")}
|
||||
</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Assign this service to regional monitoring agents for distributed monitoring
|
||||
{t("regionalMonitoringDesc")}
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -107,7 +109,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
name="regionalAgents"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Regional Agents</FormLabel>
|
||||
<FormLabel>{t("regionalAgents")}</FormLabel>
|
||||
|
||||
{/* Display selected agents */}
|
||||
{currentRegionalAgents.length > 0 && (
|
||||
@@ -140,12 +142,12 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={
|
||||
isLoading
|
||||
? "Loading agents..."
|
||||
? t("regionalAgentsLoading")
|
||||
: availableAgents.length > 0
|
||||
? "Select additional regional agents..."
|
||||
? t("regionalAgentsAvailablePlaceholder")
|
||||
: currentRegionalAgents.length > 0
|
||||
? "All available agents selected"
|
||||
: "No regional agents available"
|
||||
? t("regionalAgentsAllSelected")
|
||||
: t("regionalAgentsNoAvailable")
|
||||
} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
@@ -154,14 +156,14 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
<SelectItem value="loading" disabled>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Loading agents...
|
||||
t("regionalAgentsLoading")
|
||||
</div>
|
||||
</SelectItem>
|
||||
) : availableAgents.length === 0 ? (
|
||||
<SelectItem value="no-agents" disabled>
|
||||
{currentRegionalAgents.length > 0
|
||||
? "All available agents selected"
|
||||
: "No online regional agents available"
|
||||
? t("regionalAgentsAllSelected")
|
||||
: t("regionalAgentsNoOnlineAvailable")
|
||||
}
|
||||
</SelectItem>
|
||||
) : (
|
||||
@@ -190,13 +192,13 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
|
||||
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
|
||||
<p className="text-sm text-amber-600">
|
||||
No online regional agents found. Services will use default monitoring.
|
||||
{t("regionalAgentsNotFoundMessage")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
|
||||
<p className="text-sm text-orange-600">
|
||||
No regional agents selected. Service will use default monitoring.
|
||||
{t("regionalAgentsNotSelectedMessage")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Globe, Wifi, Server, Globe2 } from "lucide-react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { ServiceFormData } from "./types";
|
||||
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceTypeFieldProps {
|
||||
form: UseFormReturn<ServiceFormData>;
|
||||
}
|
||||
|
||||
export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||
const { t } = useLanguage();
|
||||
const getServiceIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "http":
|
||||
@@ -31,7 +33,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Service Type</FormLabel>
|
||||
<FormLabel>{t("serviceType")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
@@ -56,7 +58,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||
<span>HTTP/S</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Monitor websites and REST APIs with HTTP/HTTPS Protocol
|
||||
{t("serviceTypeHTTPDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -67,7 +69,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||
<span>PING</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Monitor host availability with PING Protocol
|
||||
{t("serviceTypePINGDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -78,7 +80,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||
<span>TCP</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Monitor TCP port connectivity with TCP Protocol
|
||||
{t("serviceTypeTCPDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -89,7 +91,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||
<span>DNS</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Monitor DNS resolution
|
||||
{t("serviceTypeDNSDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
@@ -3,12 +3,14 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescripti
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { ServiceFormData } from "./types";
|
||||
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceUrlFieldProps {
|
||||
form: UseFormReturn<ServiceFormData>;
|
||||
}
|
||||
|
||||
export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
||||
const { t } = useLanguage();
|
||||
const serviceType = form.watch("type");
|
||||
|
||||
const getPlaceholder = () => {
|
||||
@@ -22,35 +24,35 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
||||
case "dns":
|
||||
return "example.com";
|
||||
default:
|
||||
return "Enter URL or hostname";
|
||||
return t("targetDefaultPlaceholder");
|
||||
}
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
switch (serviceType) {
|
||||
case "http":
|
||||
return "Enter the full URL including protocol (http:// or https://)";
|
||||
return t("targetHTTPDesc");
|
||||
case "ping":
|
||||
return "Enter hostname or IP address to ping";
|
||||
return t("targetPINGDesc");
|
||||
case "tcp":
|
||||
return "Enter hostname or IP address for TCP connection test";
|
||||
return t("targetTCPDesc");
|
||||
case "dns":
|
||||
return "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)";
|
||||
return t("targetDNSDesc");
|
||||
default:
|
||||
return "Enter the target URL or hostname for monitoring";
|
||||
return t("targetDefaultDesc");
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldLabel = () => {
|
||||
switch (serviceType) {
|
||||
case "dns":
|
||||
return "Domain Name";
|
||||
return t("targetDNS");
|
||||
case "ping":
|
||||
return "Hostname/IP";
|
||||
case "tcp":
|
||||
return "Hostname/IP";
|
||||
default:
|
||||
return "Target URL/Host";
|
||||
return t("targetDefault");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +101,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-xs">
|
||||
Enter the port number for TCP connection test
|
||||
{t("targetTCPPortDesc")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import type { ZodSchema } from "zod";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export const serviceSchema = z.object({
|
||||
name: z.string().min(1, "Service name is required"),
|
||||
type: z.enum(["http", "ping", "tcp", "dns"]),
|
||||
url: z.string().min(1, "URL/Domain/Host is required").refine((value) => {
|
||||
// Basic validation - more specific validation can be added per type
|
||||
return value.trim().length > 0;
|
||||
}, "Please enter a valid URL, hostname, or domain"),
|
||||
port: z.string().optional(),
|
||||
interval: z.string(),
|
||||
retries: z.string(),
|
||||
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
||||
notificationChannels: z.array(z.string()).optional(),
|
||||
alertTemplate: z.string().optional(),
|
||||
// Regional monitoring fields - now supports multiple agents
|
||||
regionalMonitoringEnabled: z.boolean().optional(),
|
||||
regionalAgents: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type ServiceFormData = z.infer<typeof serviceSchema>;
|
||||
export type ServiceFormData = {
|
||||
name: string;
|
||||
type: "http" | "ping" | "tcp" | "dns";
|
||||
url: string;
|
||||
port?: string;
|
||||
interval: string;
|
||||
retries: string;
|
||||
notificationStatus?: "enabled" | "disabled";
|
||||
notificationChannels?: string[];
|
||||
alertTemplate?: string;
|
||||
regionalMonitoringEnabled?: boolean;
|
||||
regionalAgents?: string[];
|
||||
};
|
||||
|
||||
// Hook to use the schema with translations
|
||||
export const useServiceSchema = () => {
|
||||
const { t } = useLanguage();
|
||||
return z.object({
|
||||
name: z.string().min(1, t("serviceNameRequired")),
|
||||
type: z.enum(["http", "ping", "tcp", "dns"]),
|
||||
url: z.string()
|
||||
.min(1, t("urlDomainHostRequired"))
|
||||
.refine(
|
||||
(value) => value.trim().length > 0,
|
||||
t("enterValidUrlHostnameDomain")
|
||||
),
|
||||
port: z.string().optional(),
|
||||
interval: z.string(),
|
||||
retries: z.string(),
|
||||
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
||||
notificationChannels: z.array(z.string()).optional(),
|
||||
alertTemplate: z.string().optional(),
|
||||
// Regional monitoring fields - now supports multiple agents
|
||||
regionalMonitoringEnabled: z.boolean().optional(),
|
||||
regionalAgents: z.array(z.string()).optional(),
|
||||
});
|
||||
};
|
||||
@@ -25,8 +25,8 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Error Message</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Details</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("errorMessage")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("details")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -9,10 +9,12 @@ import { IncidentTable } from "./IncidentTable";
|
||||
import { StatusFilter, PageSize } from "./types";
|
||||
import { getStatusChangeEvents } from "./utils";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] }) {
|
||||
// Get current theme
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Filter state
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
@@ -65,7 +67,7 @@ export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] })
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<CardTitle className="text-card-foreground">
|
||||
<span>Incident History</span>
|
||||
<span>{t("incidentHistory")}</span>
|
||||
</CardTitle>
|
||||
<StatusFilterTabs statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} />
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { PageSize } from "./types";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface TablePaginationProps {
|
||||
currentPage: number;
|
||||
@@ -35,6 +36,7 @@ export function TablePagination({
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}: TablePaginationProps) {
|
||||
const {t} = useLanguage()
|
||||
// Generate page numbers
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
@@ -58,7 +60,7 @@ export function TablePagination({
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">Rows per page:</span>
|
||||
<span className="text-sm text-muted-foreground">{t("rowsPerPage")}:</span>
|
||||
<Select
|
||||
value={pageSize}
|
||||
onValueChange={(value) => onPageSizeChange(value as PageSize)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { Service } from "@/types/service.types";
|
||||
import { serviceService } from "@/services/serviceService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServiceRowActionsProps {
|
||||
service: Service;
|
||||
@@ -31,6 +32,7 @@ export const ServiceRowActions = ({
|
||||
onMuteAlerts
|
||||
}: ServiceRowActionsProps) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Handle pause/resume directly from dropdown
|
||||
const handlePauseResume = async (e: React.MouseEvent) => {
|
||||
@@ -122,7 +124,7 @@ export const ServiceRowActions = ({
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
<span>View Detail</span>
|
||||
<span>{t("viewDetail")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
||||
@@ -131,12 +133,12 @@ export const ServiceRowActions = ({
|
||||
{service.status === "paused" ? (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
<span>Resume Monitoring</span>
|
||||
<span>{t("resumeMonitoring")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="h-4 w-4" />
|
||||
<span>Pause Monitoring</span>
|
||||
<span>{t("pauseMonitoring")}</span>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -148,7 +150,7 @@ export const ServiceRowActions = ({
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
<span>{t("edit")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
||||
@@ -157,12 +159,12 @@ export const ServiceRowActions = ({
|
||||
{alertsMuted ? (
|
||||
<>
|
||||
<Bell className="h-4 w-4" />
|
||||
<span>Unmute Alerts</span>
|
||||
<span>{t("unmuteAlerts")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BellOff className="h-4 w-4" />
|
||||
<span>Mute Alerts</span>
|
||||
<span>{t("muteAlerts")}</span>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -175,7 +177,7 @@ export const ServiceRowActions = ({
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
<span>{t("delete")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface UptimeSummaryProps {
|
||||
uptime: number;
|
||||
@@ -7,13 +8,14 @@ interface UptimeSummaryProps {
|
||||
}
|
||||
|
||||
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="flex items-center justify-between text-xs mt-1">
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(uptime)}% uptime
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Last 20 checks
|
||||
{t('last20Checks')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,50 +1,293 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
Card, CardContent, CardDescription, CardHeader, CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Github, FileText, Twitter, MessageCircle, Code2, ServerIcon,
|
||||
} from "lucide-react";
|
||||
import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon, FolderOpen, Database, CheckCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
|
||||
export const AboutSystem: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { theme } = useTheme();
|
||||
const { systemName } = useSystemSettings();
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [mergeFields, setMergeFields] = useState(true);
|
||||
const [importResult, setImportResult] = useState<{
|
||||
success: boolean;
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
} | null>(null);
|
||||
|
||||
const [version, setVersion] = useState<string>('...');
|
||||
const [releaseDate, setReleaseDate] = useState<string>('...');
|
||||
// Helper function to check if a collection is a system collection
|
||||
const isSystemCollection = (name: string): boolean => {
|
||||
return name.startsWith('_') || ['users'].includes(name);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLatestRelease = async () => {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest');
|
||||
const data = await res.json();
|
||||
setVersion(data.tag_name || 'v1.x.x');
|
||||
setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown'));
|
||||
} catch (err) {
|
||||
setVersion('v1.x.x');
|
||||
setReleaseDate(t('unknown'));
|
||||
// Helper function to validate collection schema
|
||||
const validateCollectionSchema = (collection: any): string | null => {
|
||||
if (!collection.name) return "Collection name is required";
|
||||
if (typeof collection.name !== 'string') return "Collection name must be a string";
|
||||
if (!/^[a-z][a-z0-9_]*$/.test(collection.name)) {
|
||||
return "Collection name must start with a letter and contain only lowercase letters, numbers, and underscores";
|
||||
}
|
||||
if (collection.name.length < 3 || collection.name.length > 32) {
|
||||
return "Collection name must be between 3 and 32 characters";
|
||||
}
|
||||
|
||||
// Validate schema array if present
|
||||
if (collection.schema && !Array.isArray(collection.schema)) {
|
||||
return "Collection schema must be an array";
|
||||
}
|
||||
|
||||
// Validate each field in the schema
|
||||
if (collection.schema) {
|
||||
for (const field of collection.schema) {
|
||||
if (!field.name || typeof field.name !== 'string') {
|
||||
return `Invalid field name in collection '${collection.name}'`;
|
||||
}
|
||||
if (!field.type || typeof field.type !== 'string') {
|
||||
return `Invalid field type for '${field.name}' in collection '${collection.name}'`;
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchLatestRelease();
|
||||
}, [t]);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSchemaImport = async (schemaData: string) => {
|
||||
if (!schemaData.trim()) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "No schema data provided",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsImporting(true);
|
||||
setImportResult(null);
|
||||
|
||||
// Parse the JSON to validate it
|
||||
const collections = JSON.parse(schemaData);
|
||||
|
||||
if (!Array.isArray(collections)) {
|
||||
throw new Error("Schema must be an array of collections");
|
||||
}
|
||||
|
||||
// Check if user is authenticated as admin
|
||||
if (!pb.authStore.isValid) {
|
||||
throw new Error("Authentication required. Please log in as an admin.");
|
||||
}
|
||||
// Create/update collections one by one using PocketBase client
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
let updatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const collection of collections) {
|
||||
try {
|
||||
// Skip system collections
|
||||
if (isSystemCollection(collection.name)) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate collection schema
|
||||
const validationError = validateCollectionSchema(collection);
|
||||
if (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
// Check if collection already exists
|
||||
const existingCollections = await pb.collections.getFullList();
|
||||
const existingCollection = existingCollections.find(c => c.name === collection.name);
|
||||
|
||||
if (existingCollection) {
|
||||
if (mergeFields) {
|
||||
|
||||
try {
|
||||
// Get existing schema
|
||||
const existingSchema = existingCollection.schema || [];
|
||||
const newSchema = collection.schema || [];
|
||||
// Merge schemas - add new fields, keep existing ones
|
||||
const mergedSchema = [...existingSchema];
|
||||
let fieldsAdded = 0;
|
||||
|
||||
for (const newField of newSchema) {
|
||||
const existingFieldIndex = mergedSchema.findIndex(f => f.name === newField.name);
|
||||
if (existingFieldIndex >= 0) {
|
||||
} else {
|
||||
// Add new field
|
||||
mergedSchema.push(newField);
|
||||
fieldsAdded++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the collection with merged schema
|
||||
const updateData = {
|
||||
...collection,
|
||||
schema: mergedSchema
|
||||
};
|
||||
|
||||
const updatedCollection = await pb.collections.update(existingCollection.id, updateData);
|
||||
updatedCount++;
|
||||
} catch (mergeError) {
|
||||
throw new Error(`Failed to merge fields: ${mergeError.message || mergeError}`);
|
||||
}
|
||||
} else {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Create new collection
|
||||
try {
|
||||
const newCollection = await pb.collections.create(collection);
|
||||
successCount++;
|
||||
} catch (createError) {
|
||||
|
||||
// Extract more detailed error information
|
||||
let errorMessage = createError.message || createError;
|
||||
if (createError.data) {
|
||||
// Try to extract field-specific errors
|
||||
if (createError.data.data) {
|
||||
const fieldErrors = Object.entries(createError.data.data).map(([field, msgs]) =>
|
||||
`${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}`
|
||||
).join('; ');
|
||||
if (fieldErrors) {
|
||||
errorMessage += ` - Field errors: ${fieldErrors}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to create collection: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
const errorMsg = `Failed to process collection '${collection.name}': ${error.message || error}`;
|
||||
errors.push(errorMsg);
|
||||
}
|
||||
}
|
||||
// Set import result for display
|
||||
setImportResult({
|
||||
success: successCount + updatedCount > 0,
|
||||
created: successCount,
|
||||
updated: updatedCount,
|
||||
skipped: skippedCount
|
||||
});
|
||||
|
||||
// Show results
|
||||
const totalProcessed = successCount + updatedCount;
|
||||
if (totalProcessed > 0 || skippedCount > 0) {
|
||||
let description = '';
|
||||
if (totalProcessed > 0) {
|
||||
description = `Successfully processed ${totalProcessed} collection(s)`;
|
||||
if (successCount > 0) description += ` (${successCount} created)`;
|
||||
if (updatedCount > 0) description += ` (${updatedCount} updated)`;
|
||||
}
|
||||
if (skippedCount > 0) {
|
||||
if (description) description += `, `;
|
||||
description += `${skippedCount} skipped (system collections or existing)`;
|
||||
}
|
||||
if (errorCount > 0) description += `. ${errorCount} failed.`;
|
||||
|
||||
toast({
|
||||
title: "Import Complete",
|
||||
description,
|
||||
variant: totalProcessed > errorCount ? "default" : "destructive",
|
||||
});
|
||||
} else if (errorCount > 0) {
|
||||
toast({
|
||||
title: "Import Failed",
|
||||
description: "No collections were processed successfully.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "No Changes",
|
||||
description: "All collections already exist and no changes were made.",
|
||||
variant: "default",
|
||||
});
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
// Show detailed errors for debugging
|
||||
if (errors.length <= 3) {
|
||||
toast({
|
||||
title: "Detailed Errors",
|
||||
description: errors.join('; '),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Import Failed",
|
||||
description: error instanceof Error ? error.message : "Failed to import schema",
|
||||
variant: "destructive",
|
||||
});
|
||||
setImportResult({
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0
|
||||
});
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadLocalSchema = async () => {
|
||||
try {
|
||||
setIsImporting(true);
|
||||
setImportResult(null);
|
||||
|
||||
const response = await fetch('/upload/data/pb_schema_latest.json');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load local schema: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const schemaText = await response.text();
|
||||
|
||||
// Validate that it's valid JSON
|
||||
JSON.parse(schemaText);
|
||||
|
||||
// Directly import the schema
|
||||
await handleSchemaImport(schemaText);
|
||||
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Import Failed",
|
||||
description: error instanceof Error ? error.message : "Failed to load and import local schema file",
|
||||
variant: "destructive",
|
||||
});
|
||||
setImportResult({
|
||||
success: false,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0
|
||||
});
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
||||
<p className="text-muted-foreground text-base leading-relaxed mt-2">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<Separator />
|
||||
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
||||
<CardHeader className="bg-muted/50 pb-4">
|
||||
@@ -58,7 +301,7 @@ export const AboutSystem: React.FC = () => {
|
||||
<div className="flex flex-col space-y-3 pt-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">{t('systemVersion')}</span>
|
||||
<span className="text-foreground font-medium">{version}</span>
|
||||
<span className="text-foreground font-medium">{t('version')} 1.5.1</span>
|
||||
</div>
|
||||
<Separator className="my-1" />
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -68,22 +311,20 @@ export const AboutSystem: React.FC = () => {
|
||||
<Separator className="my-1" />
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
||||
<span className="text-foreground font-medium">{releaseDate}</span>
|
||||
<span className="text-foreground font-medium">Auguest 21, 2025</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
||||
<CardHeader className="bg-muted/50 pb-4">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
||||
<span>{t('links')}</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="font-medium text-base">
|
||||
{systemName || 'CheckCle'} {t('resources').toLowerCase()}
|
||||
</CardDescription>
|
||||
<CardDescription className="font-medium text-base">{systemName || 'CheckCle'} {t('resources').toLowerCase()}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
@@ -107,8 +348,90 @@ export const AboutSystem: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
|
||||
<CardHeader className="bg-muted/50 pb-4">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className={`h-5 w-5 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'}`} />
|
||||
<span>{t('updateSchema')}</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="font-medium text-base">{t('updateSchemaDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="merge-fields"
|
||||
checked={mergeFields}
|
||||
onCheckedChange={(checked) => setMergeFields(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="merge-fields" className="text-sm font-medium">
|
||||
{t('mergeFieldsLabel')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
onClick={handleLoadLocalSchema}
|
||||
disabled={isImporting}
|
||||
className={`flex items-center gap-2 transition-all duration-200 ${
|
||||
isImporting ? 'animate-pulse' : ''
|
||||
}`}
|
||||
>
|
||||
<Database className={`h-4 w-4 ${isImporting ? 'animate-spin' : ''}`} />
|
||||
{isImporting ? t('importing') : t('clickToUpdateSchema')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{importResult && (
|
||||
<div className={`mt-4 p-4 rounded-lg border ${
|
||||
importResult.success
|
||||
? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800'
|
||||
: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
|
||||
} animate-in fade-in-0 slide-in-from-bottom-2 duration-300`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className={`h-5 w-5 ${
|
||||
importResult.success
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`} />
|
||||
<span className={`font-medium ${
|
||||
importResult.success
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: 'text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{importResult.success ? t('importSuccessful') : t('importFailed')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`mt-2 text-sm ${
|
||||
importResult.success
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: 'text-red-700 dark:text-red-300'
|
||||
}`}>
|
||||
{importResult.created > 0 && t('collectionsCreatedCount').replace('{count}', String(importResult.created))}
|
||||
{importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + t('collectionsUpdatedCount').replace('{count}', String(importResult.updated))}
|
||||
{importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + t('collectionsSkippedCount').replace('{count}', String(importResult.skipped))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="mb-2">
|
||||
<strong>{t('instructions')}:</strong>
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>{t('mergeFields')}:</strong> {t('instructionsMergeFields')}</li>
|
||||
<li>{t('instructionsCollections')}</li>
|
||||
<li>{t('instructionsImportAuth')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutSystem;
|
||||
export default AboutSystem;
|
||||
@@ -16,6 +16,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {z} from "zod";
|
||||
|
||||
interface TemplateDialogProps {
|
||||
open: boolean;
|
||||
@@ -81,6 +82,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
||||
};
|
||||
|
||||
const renderPlaceholderGuide = () => {
|
||||
|
||||
const config = templateTypeConfigs[selectedTemplateType];
|
||||
if (!config) return null;
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ const DataRetentionSettings = () => {
|
||||
setLastCleanup(result.lastCleanup);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading retention settings:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load retention settings",
|
||||
@@ -72,7 +71,6 @@ const DataRetentionSettings = () => {
|
||||
description: "Data retention settings have been updated",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving retention settings:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to save retention settings",
|
||||
@@ -96,7 +94,6 @@ const DataRetentionSettings = () => {
|
||||
// Reload settings to get updated last cleanup time
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("Error during uptime cleanup:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to perform uptime data cleanup",
|
||||
@@ -120,7 +117,6 @@ const DataRetentionSettings = () => {
|
||||
// Reload settings to get updated last cleanup time
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("Error during server cleanup:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to perform server data cleanup",
|
||||
@@ -144,7 +140,6 @@ const DataRetentionSettings = () => {
|
||||
// Reload settings to get updated last cleanup time
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("Error during manual cleanup:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to perform database cleanup",
|
||||
@@ -210,10 +205,16 @@ const DataRetentionSettings = () => {
|
||||
min="1"
|
||||
max="365"
|
||||
value={settings.uptimeRetentionDays}
|
||||
onChange={(e) => setSettings(prev => ({
|
||||
...prev,
|
||||
uptimeRetentionDays: parseInt(e.target.value) || 30
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
uptimeRetentionDays: Number(value)
|
||||
}));
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -229,10 +230,16 @@ const DataRetentionSettings = () => {
|
||||
min="1"
|
||||
max="365"
|
||||
value={settings.serverRetentionDays}
|
||||
onChange={(e) => setSettings(prev => ({
|
||||
...prev,
|
||||
serverRetentionDays: parseInt(e.target.value) || 30
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
serverRetentionDays: Number(value)
|
||||
}));
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -250,63 +257,17 @@ const DataRetentionSettings = () => {
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleUptimeShrink}
|
||||
disabled={isUptimeShrinking}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isUptimeShrinking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Globe className="h-4 w-4" />
|
||||
)}
|
||||
Shrink Uptime Data
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleServerShrink}
|
||||
disabled={isServerShrinking}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isServerShrinking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Server className="h-4 w-4" />
|
||||
)}
|
||||
Shrink Server Data
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleFullShrink}
|
||||
disabled={isFullShrinking}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isFullShrinking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
Full Database Shrink
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -143,7 +143,7 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
|
||||
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||
<ShieldAlert 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 view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.
|
||||
<span className="font-medium">{t("permissionNotice")}</span> {t("permissionNoticeAddUser")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
@@ -153,11 +153,11 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="p-4">Loading settings...</div>;
|
||||
return <div className="p-4">{t("loadingSettings")}</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-500">Error loading settings</div>;
|
||||
return <div className="p-4 text-red-500">{t("loadingSettingsError")}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -126,15 +126,6 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
||||
<Mail className="h-5 w-5" />
|
||||
{t("sendTestEmail", "settings")}
|
||||
</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-4"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
@@ -203,9 +194,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
||||
{/* Info message */}
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.
|
||||
</AlertDescription>
|
||||
<AlertDescription>{t("testEmailAlert", "settings")}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
@@ -223,7 +212,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
||||
) : (
|
||||
<Mail className="h-4 w-4" />
|
||||
)}
|
||||
{isLoading ? t("sending", "settings") : t("send", "common")}
|
||||
{isLoading ? t("sending", "settings") : t("send", "settings")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
+205
-2
@@ -27,6 +27,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Loader2, Copy } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface NotificationChannelDialogProps {
|
||||
open: boolean;
|
||||
@@ -34,9 +35,10 @@ interface NotificationChannelDialogProps {
|
||||
editingConfig: AlertConfiguration | null;
|
||||
}
|
||||
|
||||
|
||||
const baseSchema = z.object({
|
||||
notify_name: z.string().min(1, "Name is required"),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "notifiarr", "gotify", "webhook"]),
|
||||
enabled: z.boolean().default(true),
|
||||
service_id: z.string().default("global"),
|
||||
template_id: z.string().optional(),
|
||||
@@ -78,12 +80,36 @@ const emailSchema = baseSchema.extend({
|
||||
smtp_password: z.string().min(1, "SMTP password is required"),
|
||||
});
|
||||
|
||||
const ntfySchema = baseSchema.extend({
|
||||
notification_type: z.literal("ntfy"),
|
||||
ntfy_endpoint: z.string().url("Must be a valid NTFY endpoint URL"),
|
||||
api_token: z.string().optional(),
|
||||
});
|
||||
|
||||
const pushoverSchema = baseSchema.extend({
|
||||
notification_type: z.literal("pushover"),
|
||||
api_token: z.string().min(1, "API token is required"),
|
||||
user_key: z.string().min(1, "User key is required"),
|
||||
});
|
||||
|
||||
const notifiarrSchema = baseSchema.extend({
|
||||
notification_type: z.literal("notifiarr"),
|
||||
api_token: z.string().min(1, "API token is required"),
|
||||
channel_id: z.string().min(1, "Channel ID is required"),
|
||||
});
|
||||
|
||||
const webhookSchema = baseSchema.extend({
|
||||
notification_type: z.literal("webhook"),
|
||||
webhook_url: z.string().url("Must be a valid URL"),
|
||||
webhook_payload_template: z.string().optional(),
|
||||
});
|
||||
|
||||
const gotifySchema = baseSchema.extend({
|
||||
notification_type: z.literal("gotify"),
|
||||
api_token: z.string().min(1, "API token is required"),
|
||||
server_url: z.string().url("Must be a valid server URL"),
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("notification_type", [
|
||||
telegramSchema,
|
||||
discordSchema,
|
||||
@@ -91,7 +117,11 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
||||
signalSchema,
|
||||
googleChatSchema,
|
||||
emailSchema,
|
||||
webhookSchema
|
||||
ntfySchema,
|
||||
pushoverSchema,
|
||||
notifiarrSchema,
|
||||
gotifySchema,
|
||||
webhookSchema,
|
||||
]);
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@@ -133,6 +163,31 @@ const notificationTypeOptions = [
|
||||
description: "Send notifications via email",
|
||||
icon: "/upload/notification/email.png"
|
||||
},
|
||||
{
|
||||
value: "ntfy",
|
||||
label: "NTFY",
|
||||
description: "Send notifications via NTFY push service",
|
||||
icon: "/upload/notification/ntfy.png"
|
||||
},
|
||||
{
|
||||
value: "pushover",
|
||||
label: "Pushover",
|
||||
description: "Send push notifications via Pushover",
|
||||
icon: "/upload/notification/pushover.png"
|
||||
},
|
||||
|
||||
{
|
||||
value: "notifiarr",
|
||||
label: "Notifiarr",
|
||||
description: "Send notifications via Notifiarr",
|
||||
icon: "/upload/notification/notifiarr.png"
|
||||
},
|
||||
{
|
||||
value: "gotify",
|
||||
label: "Gotify",
|
||||
description: "Send push notifications via Gotify",
|
||||
icon: "/upload/notification/gotify.png"
|
||||
},
|
||||
{
|
||||
value: "webhook",
|
||||
label: "Webhook",
|
||||
@@ -540,6 +595,154 @@ export const NotificationChannelDialog = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "ntfy" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ntfy_endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>NTFY Endpoint</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter NTFY API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional API token for authentication with NTFY server
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "pushover" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Pushover API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Pushover application API token
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>User Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Pushover user key" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Pushover user key (or group key)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "notifiarr" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Notifiarr API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Notifiarr API token for sending notifications
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Discord Channel ID" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The Discord channel ID where notifications will be sent
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "gotify" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Gotify API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Gotify application API token
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="server_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://your-gotify-server.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The URL of your Gotify server
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "webhook" && (
|
||||
<>
|
||||
<FormField
|
||||
|
||||
@@ -67,6 +67,8 @@ export const NotificationChannelList = ({
|
||||
case "signal": return "Signal";
|
||||
case "google_chat": return "Google Chat";
|
||||
case "email": return "Email";
|
||||
case "pushover": return "Pushover";
|
||||
case "notifiarr": return "Notifiarr";
|
||||
case "webhook": return "Webhook";
|
||||
default: return type || "Unknown";
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import { UserCog, Loader2, AlertCircle, Info, Users, ShieldAlert } from "lucide-
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { authService } from "@/services/authService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
const UserManagement = () => {
|
||||
const { t } = useLanguage();
|
||||
const {
|
||||
users,
|
||||
loading,
|
||||
@@ -44,21 +47,21 @@ const UserManagement = () => {
|
||||
<AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full">
|
||||
<div className="flex items-center">
|
||||
<UserCog className="h-5 w-5 mr-2 text-green-500" />
|
||||
<span>User Management</span>
|
||||
<span>{t("userManagement")}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="p-4 pt-6 bg-background rounded-b-lg">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">User Management</h2>
|
||||
<h2 className="text-2xl font-bold">{t("userManagement")}</h2>
|
||||
{isSuperAdmin && <button onClick={() => setIsAddUserDialogOpen(true)} className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center">
|
||||
<span className="mr-1">+</span> Add User
|
||||
<span className="mr-1">+</span> {t("addUser")}
|
||||
</button>}
|
||||
</div>
|
||||
|
||||
{!isSuperAdmin && <Alert className="mb-6 border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
||||
<span className="font-medium">Permission Notice:</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
|
||||
<span className="font-medium">{t("permissionNotice")}</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
|
||||
</AlertDescription>
|
||||
</Alert>}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ export const SSLCertificatesTable = () => {
|
||||
warning_threshold: updatedCertificate.warning_threshold,
|
||||
expiry_threshold: updatedCertificate.expiry_threshold,
|
||||
notification_channel: updatedCertificate.notification_channel,
|
||||
notification_id: updatedCertificate.notification_id,
|
||||
template_id: updatedCertificate.template_id,
|
||||
check_interval: updatedCertificate.check_interval,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,13 +2,24 @@ import React, { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
|
||||
import { SSLCertificatesTable } from "./SSLCertificatesTable";
|
||||
import { LoadingState } from "@/components/services/LoadingState";
|
||||
import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
|
||||
import {
|
||||
fetchSSLCertificates,
|
||||
addSSLCertificate,
|
||||
checkAndUpdateCertificate,
|
||||
refreshAllCertificates,
|
||||
deleteSSLCertificate,
|
||||
} from "@/services/ssl";
|
||||
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
|
||||
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
|
||||
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
|
||||
@@ -21,18 +32,23 @@ export const SSLDomainContent = () => {
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [refreshingId, setRefreshingId] = useState<string | null>(null);
|
||||
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
|
||||
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
|
||||
const [selectedCertificate, setSelectedCertificate] =
|
||||
useState<SSLCertificate | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
// Fetch SSL certificates with explicit error handling
|
||||
const { data: certificates = [], isLoading, error } = useQuery({
|
||||
queryKey: ['ssl-certificates'],
|
||||
const {
|
||||
data: certificates = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["ssl-certificates"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const result = await fetchSSLCertificates();
|
||||
return result;
|
||||
} catch (error) {
|
||||
toast.error(t('failedToLoadCertificates'));
|
||||
toast.error(t("failedToLoadCertificates"));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -44,101 +60,114 @@ export const SSLDomainContent = () => {
|
||||
const addMutation = useMutation({
|
||||
mutationFn: addSSLCertificate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
setIsAddDialogOpen(false);
|
||||
toast.success(t('sslCertificateAdded'));
|
||||
toast.success(t("sslCertificateAdded"));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
|
||||
}
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("failedToAddCertificate")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Edit certificate mutation - Updated to include notification_id and template_id
|
||||
const editMutation = useMutation({
|
||||
mutationFn: async (certificate: SSLCertificate) => {
|
||||
|
||||
// Create the update data object with new fields
|
||||
const updateData = {
|
||||
warning_threshold: Number(certificate.warning_threshold),
|
||||
expiry_threshold: Number(certificate.expiry_threshold),
|
||||
notification_channel: certificate.notification_channel,
|
||||
notification_id: certificate.notification_id || '', // Multi notification channels
|
||||
template_id: certificate.template_id || '', // Alert template ID
|
||||
notification_id: certificate.notification_id || "", // Multi notification channels
|
||||
template_id: certificate.template_id || "", // Alert template ID
|
||||
check_interval: certificate.check_interval,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Update certificate in the database using PocketBase directly
|
||||
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
|
||||
|
||||
const updated = await pb
|
||||
.collection("ssl_certificates")
|
||||
.update(certificate.id, updateData);
|
||||
|
||||
// After updating the settings, refresh the certificate to ensure it's up to date
|
||||
// This will also check if notification needs to be sent based on updated thresholds
|
||||
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
|
||||
|
||||
|
||||
return refreshedCert;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
setIsEditDialogOpen(false);
|
||||
setSelectedCertificate(null);
|
||||
toast.success(t('sslCertificateUpdated'));
|
||||
toast.success(t("sslCertificateUpdated"));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
|
||||
}
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("failedToUpdateCertificate")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Delete certificate mutation
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: deleteSSLCertificate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
toast.success(t('sslCertificateDeleted'));
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
toast.success(t("sslCertificateDeleted"));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
|
||||
}
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("failedToDeleteCertificate")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Refresh certificate mutation - Updated to remove individual toast notifications
|
||||
const refreshMutation = useMutation({
|
||||
mutationFn: checkAndUpdateCertificate,
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
setRefreshingId(null);
|
||||
// Removed individual success toast notification
|
||||
},
|
||||
onError: (error) => {
|
||||
setRefreshingId(null);
|
||||
|
||||
|
||||
// Still refresh the data to show any partial information that was saved
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
// Removed individual error toast notification
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Refresh all certificates mutation
|
||||
const refreshAllMutation = useMutation({
|
||||
mutationFn: refreshAllCertificates,
|
||||
onSuccess: (result) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
setIsRefreshingAll(false);
|
||||
|
||||
|
||||
if (result.failed === 0) {
|
||||
toast.success(t('allCertificatesRefreshed').replace('{count}', result.success.toString()));
|
||||
toast.success(
|
||||
t("allCertificatesRefreshed").replace(
|
||||
"{count}",
|
||||
result.success.toString()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
toast.info(t('someCertificatesFailed')
|
||||
.replace('{success}', result.success.toString())
|
||||
.replace('{failed}', result.failed.toString()));
|
||||
toast.info(
|
||||
t("someCertificatesFailed")
|
||||
.replace("{success}", result.success.toString())
|
||||
.replace("{failed}", result.failed.toString())
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(t('failedToCheckCertificate'));
|
||||
toast.error(t("failedToCheckCertificate"));
|
||||
setIsRefreshingAll(false);
|
||||
|
||||
|
||||
// Still refresh the data to show any partial information
|
||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
|
||||
@@ -166,12 +195,14 @@ export const SSLDomainContent = () => {
|
||||
|
||||
const handleRefreshAll = async () => {
|
||||
if (certificates.length === 0) {
|
||||
toast.info(t('noCertificatesToRefresh'));
|
||||
toast.info(t("noCertificatesToRefresh"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsRefreshingAll(true);
|
||||
toast.info(t('startingRefreshAll').replace('{count}', certificates.length.toString()));
|
||||
toast.info(
|
||||
t("startingRefreshAll").replace("{count}", certificates.length.toString())
|
||||
);
|
||||
refreshAllMutation.mutate();
|
||||
};
|
||||
|
||||
@@ -182,48 +213,60 @@ export const SSLDomainContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
||||
<p>{t('failedToLoadCertificates')}</p>
|
||||
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>
|
||||
{t('check')}
|
||||
<p>{t("failedToLoadCertificates")}</p>
|
||||
<Button
|
||||
onClick={() =>
|
||||
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] })
|
||||
}
|
||||
>
|
||||
{t("check")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
||||
<div className="flex flex-col flex-1">
|
||||
<main className="flex-1 flex flex-col overflow-auto bg-background pb-0 dark:bg-black p-2 ">
|
||||
<div className="flex flex-col flex-1 bg-white dark:bg-black p-6 rounded-xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-foreground">{t('sslDomainManagement')}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p>
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{t("sslDomainManagement")}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("monitorSSLCertificates")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRefreshAll}
|
||||
disabled={isRefreshingAll || refreshingId !== null}
|
||||
className="relative"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
|
||||
{t('refreshAll')}
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 mr-2 ${
|
||||
isRefreshingAll ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
{t("refreshAll")}
|
||||
{isRefreshingAll && (
|
||||
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
|
||||
...
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
className="text-primary-foreground"
|
||||
onClick={() => setIsAddDialogOpen(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" /> {t('addDomain')}
|
||||
<Plus className="w-4 h-4 mr-2" /> {t("addDomain")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<SSLCertificateStatusCards certificates={certificates} />
|
||||
|
||||
|
||||
<div className="mt-6 flex-1 flex flex-col pb-6">
|
||||
<SSLCertificatesTable />
|
||||
</div>
|
||||
@@ -232,26 +275,29 @@ export const SSLDomainContent = () => {
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
|
||||
<DialogTitle>{t("addSSLCertificate")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AddSSLCertificateForm
|
||||
onSubmit={handleAddCertificate}
|
||||
onCancel={() => setIsAddDialogOpen(false)}
|
||||
<AddSSLCertificateForm
|
||||
onSubmit={handleAddCertificate}
|
||||
onCancel={() => setIsAddDialogOpen(false)}
|
||||
isPending={addMutation.isPending}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{selectedCertificate && (
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
|
||||
setIsEditDialogOpen(open);
|
||||
if (!open) setSelectedCertificate(null);
|
||||
}}>
|
||||
<Dialog
|
||||
open={isEditDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsEditDialogOpen(open);
|
||||
if (!open) setSelectedCertificate(null);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
|
||||
<DialogTitle>{t("editSSLCertificate")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<EditSSLCertificateForm
|
||||
<EditSSLCertificateForm
|
||||
certificate={selectedCertificate}
|
||||
onSubmit={handleUpdateCertificate}
|
||||
onCancel={() => {
|
||||
@@ -265,4 +311,4 @@ export const SSLDomainContent = () => {
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// src/contexts/FontSizeContext.js
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useDynamicFontSize = (size) => {
|
||||
useEffect(() => {
|
||||
if (typeof size === "number") {
|
||||
// Apply font size to the root HTML element
|
||||
document.documentElement.style.fontSize = `${size}px`;
|
||||
localStorage.setItem("fontSize", size.toString());
|
||||
}
|
||||
}, [size]);
|
||||
};
|
||||
|
||||
export default useDynamicFontSize;
|
||||
@@ -1,16 +1,22 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from "react";
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
|
||||
import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
|
||||
|
||||
type TranslationVars = Record<string, string | number>;
|
||||
|
||||
type LanguageContextType = {
|
||||
language: Language;
|
||||
setLanguage: (language: Language) => void;
|
||||
t: <M extends TranslationModule>(key: string, module?: M) => string;
|
||||
t: {
|
||||
(key: string): string;
|
||||
(key: string, vars: TranslationVars): string;
|
||||
<M extends TranslationModule>(key: string, module: M): string;
|
||||
<M extends TranslationModule>(key: string, module: M, vars: TranslationVars): string;
|
||||
};
|
||||
};
|
||||
|
||||
// ❗ Create the context with `undefined` to enforce provider usage
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
// ✅ Stable custom hook
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useLanguage = () => {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
@@ -21,27 +27,82 @@ export const useLanguage = () => {
|
||||
|
||||
export const LanguageProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [language, setLanguage] = useState<Language>("en");
|
||||
const fallbackLanguage: Language = "en";
|
||||
|
||||
const t = <M extends TranslationModule>(key: string, module?: M): string => {
|
||||
if (module) {
|
||||
const translatedValue = translations[language][module][key as TranslationKey<M>];
|
||||
return typeof translatedValue === "string" ? translatedValue : key;
|
||||
const interpolate = (template: string, vars: TranslationVars): string => {
|
||||
return template.replace(/{(\w+)}/g, (match, key) => {
|
||||
return vars[key] !== undefined ? String(vars[key]) : match;
|
||||
});
|
||||
};
|
||||
|
||||
const t = useCallback(((
|
||||
key: string,
|
||||
moduleOrVars?: TranslationModule | TranslationVars,
|
||||
vars?: TranslationVars
|
||||
): string => {
|
||||
let module: TranslationModule | undefined;
|
||||
let variables: TranslationVars | undefined;
|
||||
|
||||
if (typeof moduleOrVars === "string") {
|
||||
module = moduleOrVars;
|
||||
variables = vars;
|
||||
} else if (typeof moduleOrVars === "object") {
|
||||
variables = moduleOrVars;
|
||||
}
|
||||
|
||||
for (const mod in translations[language]) {
|
||||
const moduleKey = mod as TranslationModule;
|
||||
const translatedValue = translations[language][moduleKey][key as any];
|
||||
if (translatedValue && typeof translatedValue === "string") {
|
||||
return translatedValue;
|
||||
const langPack = translations[language];
|
||||
const fallbackPack = translations[fallbackLanguage];
|
||||
let translatedText = "";
|
||||
|
||||
if (module) {
|
||||
const moduleKey = key as TranslationKey<typeof module>;
|
||||
const valCur = langPack?.[module]?.[moduleKey];
|
||||
if (typeof valCur === "string") {
|
||||
translatedText = valCur;
|
||||
} else {
|
||||
const valEn = fallbackPack?.[module]?.[moduleKey];
|
||||
translatedText = typeof valEn === "string" ? valEn : key;
|
||||
}
|
||||
} else {
|
||||
let found = false;
|
||||
|
||||
for (const mod of Object.keys(langPack) as TranslationModule[]) {
|
||||
const moduleTranslations = langPack[mod];
|
||||
if (moduleTranslations && key in moduleTranslations) {
|
||||
const val = moduleTranslations[key as keyof typeof moduleTranslations];
|
||||
if (typeof val === "string") {
|
||||
translatedText = val;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
for (const mod of Object.keys(fallbackPack) as TranslationModule[]) {
|
||||
const moduleTranslations = fallbackPack[mod];
|
||||
if (moduleTranslations && key in moduleTranslations) {
|
||||
const val = moduleTranslations[key as keyof typeof moduleTranslations];
|
||||
if (typeof val === "string") {
|
||||
translatedText = val;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
translatedText = key;
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
return variables ? interpolate(translatedText, variables) : translatedText;
|
||||
}) as LanguageContextType['t'], [language]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,38 +1,48 @@
|
||||
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
type SideBar = boolean;
|
||||
|
||||
interface SidebarContextType {
|
||||
sidebarCollapsed: boolean;
|
||||
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||
sidebarCollapsed: SideBar;
|
||||
setSidebarCollapsed: React.Dispatch<React.SetStateAction<SideBar>>;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||
|
||||
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
interface SidebarProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export const SidebarProvider: React.FC<SidebarProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
// Initialize the state
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState<SideBar>(() => {
|
||||
const saved = localStorage.getItem("sidebarCollapsed");
|
||||
return saved === "true";
|
||||
});
|
||||
const toggleSidebar = () => {
|
||||
setSidebarCollapsed(prev => !prev);
|
||||
setSidebarCollapsed((prev) => {
|
||||
const newState = !prev;
|
||||
localStorage.setItem("sidebarCollapsed", newState.toString());
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const value = {
|
||||
sidebarCollapsed,
|
||||
setSidebarCollapsed,
|
||||
toggleSidebar
|
||||
toggleSidebar,
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={value}>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// A custom hook to consume the sidebar context.
|
||||
export const useSidebar = () => {
|
||||
const context = useContext(SidebarContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider');
|
||||
throw new Error("useSidebar must be used within a SidebarProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
@@ -21,61 +20,69 @@ const InstanceMonitoring = () => {
|
||||
const { t } = useLanguage();
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const [stats, setStats] = useState<ServerStats>({
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0,
|
||||
warning: 0
|
||||
warning: 0,
|
||||
});
|
||||
|
||||
|
||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
|
||||
const { data: servers = [], isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['servers'],
|
||||
|
||||
const {
|
||||
data: servers = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["servers"],
|
||||
queryFn: serverService.getServers,
|
||||
refetchInterval: 30000 // Refetch every 30 seconds
|
||||
refetchInterval: 30000, // Refetch every 30 seconds
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (servers.length > 0) {
|
||||
serverService.getServerStats(servers).then(setStats);
|
||||
}
|
||||
}, [servers]);
|
||||
|
||||
|
||||
const handleRefresh = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate('/login');
|
||||
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}
|
||||
<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">
|
||||
<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>
|
||||
<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.
|
||||
Unable to fetch server data. Please check your connection and
|
||||
try again.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
<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
|
||||
@@ -86,22 +93,22 @@ const InstanceMonitoring = () => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<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}
|
||||
<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">
|
||||
<main className="flex-1 overflow-auto dark:bg-black p-2 shadow">
|
||||
<div className="p-4 sm:p-2 lg:p-2 space-y-6 h-full bg-white dark:bg-black p-4 rounded-xl">
|
||||
{/* Header Section */}
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-2 ">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-2xl lg:text-2xl font-bold text-foreground">
|
||||
Instance Monitoring
|
||||
@@ -110,7 +117,10 @@ const InstanceMonitoring = () => {
|
||||
Monitor and manage your server instances in real-time
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
|
||||
<Button
|
||||
onClick={() => setAddDialogOpen(true)}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Server Agent
|
||||
</Button>
|
||||
@@ -118,13 +128,17 @@ const InstanceMonitoring = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats Cards Section */}
|
||||
<div>
|
||||
<div className="p-2 ">
|
||||
<ServerStatsCards stats={stats} />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Server Table Section */}
|
||||
<div>
|
||||
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
|
||||
<div className="p-3">
|
||||
<ServerTable
|
||||
servers={servers}
|
||||
isLoading={isLoading}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -139,4 +153,4 @@ const InstanceMonitoring = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceMonitoring;
|
||||
export default InstanceMonitoring;
|
||||
|
||||
@@ -10,8 +10,11 @@ import { User } from "@/services/userService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
const Profile = () => {
|
||||
const { t } = useLanguage()
|
||||
|
||||
// Use shared sidebar state
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
|
||||
@@ -84,12 +87,12 @@ const Profile = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<p>Please login to view your profile</p>
|
||||
<p>{t("loginToViewProfile")}</p>
|
||||
<button
|
||||
onClick={() => navigate("/login")}
|
||||
className="mt-4 px-4 py-2 bg-primary text-white rounded"
|
||||
>
|
||||
Go to Login
|
||||
{t("goToLogin")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +113,7 @@ const Profile = () => {
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<span className="ml-2">Loading user data...</span>
|
||||
<span className="ml-2">{t("loadingUserData")}</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
@@ -121,7 +124,7 @@ const Profile = () => {
|
||||
onClick={() => fetchUserData()}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
||||
>
|
||||
Retry
|
||||
{t("retry")}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -136,4 +139,4 @@ const Profile = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
export default Profile;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { authService } from "@/services/authService";
|
||||
@@ -7,32 +6,41 @@ import { Header } from "@/components/dashboard/Header";
|
||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
|
||||
import { LoadingState } from "@/components/services/LoadingState";
|
||||
import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
|
||||
import {
|
||||
fetchSSLCertificates,
|
||||
shouldRunDailyCheck,
|
||||
checkAllCertificatesAndNotify,
|
||||
} from "@/services/sslCertificateService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
|
||||
const SslDomain = () => {
|
||||
// Get language context for translations
|
||||
const { t } = useLanguage();
|
||||
|
||||
|
||||
// Use shared sidebar state
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
|
||||
// Get current user
|
||||
const currentUser = authService.getCurrentUser();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
// Fetch SSL certificates with error handling and debugging
|
||||
const { data: certificates = [], isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['ssl-certificates'],
|
||||
const {
|
||||
data: certificates = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["ssl-certificates"],
|
||||
queryFn: async () => {
|
||||
// console.log("Fetching SSL certificates from SslDomain page...");
|
||||
// console.log("Fetching SSL certificates from SslDomain page...");
|
||||
try {
|
||||
const result = await fetchSSLCertificates();
|
||||
// console.log("SSL certificates fetch successful, count:", result.length);
|
||||
// console.log("SSL certificates fetch successful, count:", result.length);
|
||||
return result;
|
||||
} catch (err) {
|
||||
// console.error("Error fetching SSL certificates from page:", err);
|
||||
// console.error("Error fetching SSL certificates from page:", err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -40,23 +48,23 @@ const SslDomain = () => {
|
||||
refetchInterval: 300000, // Refresh every 5 minutes
|
||||
retry: 3, // Retry failed requests 3 times
|
||||
});
|
||||
|
||||
|
||||
// Check all SSL certificates once per day
|
||||
useEffect(() => {
|
||||
const checkCertificates = async () => {
|
||||
// Check if we should run daily check
|
||||
if (shouldRunDailyCheck()) {
|
||||
// console.log("Running daily SSL certificate check...");
|
||||
// console.log("Running daily SSL certificate check...");
|
||||
await checkAllCertificatesAndNotify();
|
||||
// Refresh certificate list after daily check
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Run check when component mounts
|
||||
checkCertificates();
|
||||
}, [refetch]);
|
||||
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
@@ -69,11 +77,11 @@ const SslDomain = () => {
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex flex-col flex-1">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<LoadingState />
|
||||
</div>
|
||||
@@ -86,23 +94,25 @@ const SslDomain = () => {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex flex-col flex-1">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
<div className="flex flex-col flex-1 ">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center h-full p-6">
|
||||
<h2 className="text-xl font-bold mb-2">{t('failedToLoadCertificates')}</h2>
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 ">
|
||||
<h2 className="text-xl font-bold mb-2">
|
||||
{t("failedToLoadCertificates")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{error instanceof Error ? error.message : t('unknown')}
|
||||
{error instanceof Error ? error.message : t("unknown")}
|
||||
</p>
|
||||
<button
|
||||
<button
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
{t('check')}
|
||||
{t("check")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,12 +124,12 @@ const SslDomain = () => {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex flex-col flex-1">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
<div className="flex flex-col flex-1 ">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<SSLDomainContent />
|
||||
</div>
|
||||
@@ -127,4 +137,4 @@ const SslDomain = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SslDomain;
|
||||
export default SslDomain;
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
|
||||
collectionId?: string;
|
||||
collectionName?: string;
|
||||
service_id: string;
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "notifiarr" | "gotify" | "webhook";
|
||||
telegram_chat_id?: string;
|
||||
discord_webhook_url?: string;
|
||||
signal_number?: string;
|
||||
@@ -28,7 +28,10 @@ export interface AlertConfiguration {
|
||||
smtp_password?: string;
|
||||
webhook_id?: string;
|
||||
channel_id?: string;
|
||||
// Webhook fields for alert_configurations
|
||||
ntfy_endpoint?: string;
|
||||
api_token?: string;
|
||||
user_key?: string;
|
||||
server_url?: string;
|
||||
webhook_url?: string;
|
||||
webhook_payload_template?: string;
|
||||
}
|
||||
@@ -82,9 +85,24 @@ export const alertConfigService = {
|
||||
cleanConfig.smtp_server = config.smtp_server || "";
|
||||
cleanConfig.smtp_port = config.smtp_port || "";
|
||||
cleanConfig.smtp_password = config.smtp_password || "";
|
||||
|
||||
} else if (config.notification_type === "ntfy") {
|
||||
cleanConfig.ntfy_endpoint = config.ntfy_endpoint || "";
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
|
||||
} else if (config.notification_type === "webhook") {
|
||||
} else if (config.notification_type === "pushover") {
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
cleanConfig.user_key = config.user_key || "";
|
||||
|
||||
} else if (config.notification_type === "notifiarr") {
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
cleanConfig.channel_id = config.channel_id || "";
|
||||
|
||||
} else if (config.notification_type === "gotify") {
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
cleanConfig.server_url = config.server_url || "";
|
||||
|
||||
} else if (config.notification_type === "webhook") {
|
||||
cleanConfig.webhook_url = config.webhook_url || "";
|
||||
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
|
||||
affected_systems: data.affected_systems,
|
||||
priority: data.priority.toLowerCase(),
|
||||
service_id: data.service_id || '',
|
||||
assigned_to: data.assigned_to || '', // Direct user ID assignment
|
||||
//assigned_to: data.assigned_to || '', // Direct user ID assignment
|
||||
//2025-08-18: // map UI field -> server schema
|
||||
assigned_users: typeof data.assigned_to === 'string' ? data.assigned_to : '',
|
||||
root_cause: data.root_cause || '',
|
||||
resolution_steps: data.resolution_steps || '',
|
||||
lessons_learned: data.lessons_learned || '',
|
||||
@@ -82,28 +84,41 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
|
||||
export const updateIncident = async (id: string, data: Partial<IncidentItem>): Promise<void> => {
|
||||
try {
|
||||
console.log(`Updating incident ${id} with:`, data);
|
||||
|
||||
// Make sure impact and priority are lowercase
|
||||
const payload = {
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
...data,
|
||||
impact: data.impact?.toLowerCase(),
|
||||
priority: data.priority?.toLowerCase(),
|
||||
status: data.status ? formatStatus(data.status) : undefined,
|
||||
impact_status: data.status ? data.status.toLowerCase() : undefined,
|
||||
...(data.status?.toLowerCase() === 'resolved' && !data.resolution_time
|
||||
? { resolution_time: new Date().toISOString() }
|
||||
: {})
|
||||
...(data.impact ? { impact: data.impact.toLowerCase() } : {}),
|
||||
...(data.priority ? { priority: data.priority.toLowerCase() } : {}),
|
||||
...(data.status
|
||||
? {
|
||||
status: formatStatus(data.status),
|
||||
impact_status: data.status.toLowerCase(),
|
||||
}
|
||||
: {}),
|
||||
// set only if assigned_to was provided
|
||||
...(typeof data.assigned_to === 'string'
|
||||
? {
|
||||
assigned_users: data.assigned_to, // server field
|
||||
assigned_to: data.assigned_to, // legacy field for backward compatibility
|
||||
}
|
||||
: {}),
|
||||
// add resolution_time only if status changes to resolved
|
||||
...((data.status?.toLowerCase() === 'resolved' && !data.resolution_time)
|
||||
? { resolution_time: new Date().toISOString() }
|
||||
: {}),
|
||||
};
|
||||
|
||||
|
||||
console.log("Final payload for update:", payload);
|
||||
await pb.collection('incidents').update(id, payload);
|
||||
|
||||
|
||||
// Invalidate cache after update
|
||||
invalidateCache();
|
||||
|
||||
|
||||
console.log(`Incident ${id} updated successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error updating incident:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import jsPDF from 'jspdf';
|
||||
import { IncidentItem } from '../types';
|
||||
import { userService } from '@/services/userService';
|
||||
import {
|
||||
addBasicInfoSection,
|
||||
addDescriptionSection,
|
||||
@@ -22,6 +23,18 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
|
||||
throw new Error('Invalid incident data');
|
||||
}
|
||||
|
||||
// Fetch assigned user data if available
|
||||
let assignedUser: { full_name?: string; username?: string } | null = null;
|
||||
const assigneeId = incident?.assigned_users || incident?.assigned_to;
|
||||
if (assigneeId) {
|
||||
try {
|
||||
assignedUser = await userService.getUser(assigneeId);
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch assigned user for PDF:', error);
|
||||
// Continue without user data
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new PDF document with portrait orientation
|
||||
const doc = new jsPDF({
|
||||
@@ -85,7 +98,7 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
|
||||
}
|
||||
|
||||
// Add assignment section
|
||||
yPos = addAssignmentSection(doc, incident, yPos);
|
||||
yPos = addAssignmentSection(doc, incident, yPos, assignedUser);
|
||||
|
||||
// Check if we need to add a new page
|
||||
if (yPos > 250) {
|
||||
|
||||
@@ -146,7 +146,7 @@ export const addResolutionSection = (doc: jsPDF, incident: IncidentItem, yPos: n
|
||||
/**
|
||||
* Add the assignment information section to the PDF
|
||||
*/
|
||||
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number, assignedUser?: { full_name?: string; username?: string } | null): number => {
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Assignment Information', 15, yPos);
|
||||
@@ -158,8 +158,16 @@ export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: n
|
||||
yPos += 10;
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
const assignedTo = incident.assigned_to || 'Not assigned';
|
||||
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
|
||||
|
||||
if (assignedUser) {
|
||||
// Show user name if available
|
||||
const userName = assignedUser.full_name || assignedUser.username || 'Unknown User';
|
||||
doc.text(`Assigned to: ${userName}`, 15, yPos);
|
||||
} else {
|
||||
// Fallback to ID if no user data available
|
||||
const assignedTo = incident.assigned_users || incident.assigned_to || 'Not assigned';
|
||||
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
|
||||
}
|
||||
|
||||
return yPos + 10;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ export type IncidentItem = {
|
||||
service_id?: string;
|
||||
timestamp?: string;
|
||||
description: string;
|
||||
assigned_to?: string;
|
||||
assigned_to?: string; // legacy UI field
|
||||
assigned_users?: string; // server field
|
||||
resolution_time?: string;
|
||||
impact: string;
|
||||
affected_systems: string;
|
||||
@@ -32,7 +33,8 @@ export type CreateIncidentInput = {
|
||||
affected_systems: string;
|
||||
priority: string;
|
||||
service_id?: string;
|
||||
assigned_to?: string;
|
||||
assigned_to?: string; // legacy UI field
|
||||
assigned_users?: string; // server field
|
||||
root_cause?: string;
|
||||
resolution_steps?: string;
|
||||
lessons_learned?: string;
|
||||
@@ -50,7 +52,8 @@ export type UpdateIncidentInput = {
|
||||
affected_systems?: string;
|
||||
priority?: string;
|
||||
service_id?: string;
|
||||
assigned_to?: string;
|
||||
assigned_to?: string; // legacy UI field
|
||||
assigned_users?: string; // server field
|
||||
root_cause?: string;
|
||||
resolution_steps?: string;
|
||||
lessons_learned?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { AboutTranslations } from '../types/about';
|
||||
|
||||
export const aboutTranslations: AboutTranslations = {
|
||||
aboutCheckcle: "Über Checkcle",
|
||||
aboutCheckCle: "Über Checkcle",
|
||||
systemDescription: "Checkcle ist ein Open-Source-Überwachungs-Stack, der Echtzeit-Einblicke in Server- und Dienstzustände, Vorfallmanagement und operative Transparenz bietet. Veröffentlicht unter der MIT-Lizenz.",
|
||||
systemVersion: "Systemversion",
|
||||
license: "Lizenz",
|
||||
@@ -15,6 +15,7 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActions: "Schnelle Aktionen",
|
||||
quickActionsDescription: "Greifen Sie schnell auf gängige Überwachungsvorgänge und -funktionen zu. Wählen Sie unten eine Aktion aus, um loszulegen.",
|
||||
quickTips: "Schnelle Tipps",
|
||||
releasedOn: "Veröffentlicht",
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ export const commonTranslations: CommonTranslations = {
|
||||
logout: "Abmelden",
|
||||
language: "Sprache",
|
||||
english: "Englisch",
|
||||
khmer: "Khmer",
|
||||
khmer: "ភាសាខ្មែរ",
|
||||
german: "Deutsch",
|
||||
simplifiedChinese: "Simplified Chinese",
|
||||
korean: "한국어",
|
||||
japanese: "日本語",
|
||||
simplifiedChinese: "简体中文",
|
||||
goodMorning: "Guten Morgen",
|
||||
goodAfternoon: "Guten Nachmittag",
|
||||
goodEvening: "Guten Abend",
|
||||
|
||||
@@ -51,4 +51,13 @@ export const incidentTranslations: IncidentTranslations = {
|
||||
configuration: 'Konfiguration',
|
||||
failedToUpdateStatus: 'Fehler beim Aktualisieren des Status',
|
||||
inProgress: 'In Bearbeitung',
|
||||
enterRootCause: 'Grundursache eingeben',
|
||||
enterIncidentTitle: 'Vorfalltitel eingeben',
|
||||
enterIncidentDescription: 'Vorfallbeschreibung eingeben',
|
||||
enterServiceId: 'Service-IDs durch Komma getrennt eingeben',
|
||||
selectAssignedUser: 'Zugewiesenen Benutzer auswählen',
|
||||
noAssignedUser: 'Der Vorfall ist derzeit keinem Benutzer zugewiesen',
|
||||
affectedSystems: 'Betroffene Systeme',
|
||||
enterAffectedSystems: 'Betroffene Systeme eingeben',
|
||||
separateSystemsWithComma: 'Betroffene Systeme durch Komma getrennt eingeben',
|
||||
};
|
||||
|
||||
@@ -64,4 +64,17 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
noScheduledMaintenance: 'Keine geplanten Wartungen',
|
||||
noMaintenanceWindows: 'Für diesen Zeitraum sind keine Wartungsfenster vorhanden. Erstellen Sie eines über die Schaltfläche „Wartung erstellen“.',
|
||||
maintenanceCreatedSuccess: 'Wartungsfenster erfolgreich erstellt',
|
||||
editMaintenanceWindow: 'Wartung bearbeiten',
|
||||
editMaintenanceDesc: 'Wartungsdetails bearbeiten',
|
||||
priorityDescription: 'Wartungspriorität auswählen',
|
||||
statusDescription: 'Wartungsstatus auswählen',
|
||||
impactLevel: 'Auswirkungsgrad',
|
||||
impactLevelDescription: 'Auswirkungsgrad auswählen',
|
||||
selectAssignedUsers: 'Zugewiesene Benutzer auswählen',
|
||||
assignedPersonnelDescription: 'Derzeit zugewiesene Benutzer',
|
||||
mutedNotifications: 'Benachrichtigungen deaktiviert',
|
||||
notificationsAreMuted: 'Benachrichtigungen sind für diese Wartung derzeit deaktiviert',
|
||||
selectNotificationChannel: 'Benachrichtigungskanal hinzufügen',
|
||||
enableNotificationsFirst: 'Aktivieren Sie zuerst Benachrichtigungen, um einen Kanal auszuwählen',
|
||||
updateMaintenance: 'Wartung aktualisieren',
|
||||
};
|
||||
|
||||
@@ -1,13 +1,98 @@
|
||||
|
||||
import { ServicesTranslations } from '../types/services';
|
||||
|
||||
export const servicesTranslations: ServicesTranslations = {
|
||||
// Dienst-Tabelle
|
||||
serviceName: "Dienstname",
|
||||
serviceType: "Diensttyp",
|
||||
// Service Table
|
||||
serviceStatus: "Dienststatus",
|
||||
responseTime: "Antwortzeit",
|
||||
uptime: "Betriebszeit",
|
||||
lastChecked: "Zuletzt überprüft",
|
||||
noServices: "Keine Dienste entsprechen Ihren Filterkriterien.",
|
||||
currentlyMonitoring: "Derzeit überwacht",
|
||||
retry: "Wiederholen",
|
||||
overview: "Überblick",
|
||||
newService: "Neuer Dienst",
|
||||
rowsPerPage: "Zeilen pro Seite",
|
||||
search: "Suchen",
|
||||
allTypes: "Alle Arten",
|
||||
createNewService: "Neuen Service hinzufügen",
|
||||
createNewServiceDesc: "Geben Sie detaillierte Informationen ein, um einen neuen zu erstellen, den Sie überwachen möchten.",
|
||||
|
||||
// ServiceBasicFields.tsx
|
||||
serviceName: "Dienstname",
|
||||
serviceNameDesc: "Geben Sie einen aussagekräftigen Namen für Ihren Service ein",
|
||||
|
||||
// ServiceConfigFields.tsx
|
||||
checkInterval: "Prüfintervall",
|
||||
seconds: "Sekunden",
|
||||
minute: "Minute",
|
||||
minutes: "Minuten",
|
||||
hour: "Stunde",
|
||||
hours: "Stunden",
|
||||
custom: "Benutzerdefiniert",
|
||||
checkIntervalPlaceholder: "Intervall in Sekunden eingeben",
|
||||
backToPresets: "Zurück zu Voreinstellungen",
|
||||
checkIntervalDesc: "Wie oft der Dienststatus überprüft werden soll",
|
||||
checkIntervalDescCustom: "Benutzerdefiniertes Intervall in Sekunden eingeben (mindestens 10 Sekunden)",
|
||||
retryAttempts: "Wiederholungsversuche",
|
||||
attempt: "Versuch",
|
||||
attempts: "Versuche",
|
||||
retryAttemptsDesc: "Anzahl der Wiederholungsversuche, bevor der Dienst als ausgefallen markiert wird",
|
||||
|
||||
// ServiceForm.tsx
|
||||
updateService: "Dienst aktualisieren",
|
||||
createService: "Dienst erstellen",
|
||||
|
||||
// ServiceNotificationFields.tsx
|
||||
enableNotifications: "Benachrichtigungen aktivieren",
|
||||
enableNotificationsDesc: "Benachrichtigungen für diesen Dienst aktivieren oder deaktivieren",
|
||||
notificationChannels: "Benachrichtigungskanäle",
|
||||
notificationChannelsEnabledDesc: "Benachrichtigungskanäle für diesen Dienst auswählen",
|
||||
notificationChannelsDesc: "Aktivieren Sie zuerst Benachrichtigungen, um Kanäle auszuwählen",
|
||||
notificationChannelsPlaceholder: "Benachrichtigungskanal hinzufügen",
|
||||
alertTemplate: "Alarmvorlage",
|
||||
alertTemplateLoading: "Lade Vorlagen...",
|
||||
alertTemplatePlaceholder: "Alarmvorlage auswählen",
|
||||
alertTemplateEnabledDesc: "Wählen Sie eine Vorlage für Alarmmeldungen",
|
||||
alertTemplateDesc: "Aktivieren Sie zuerst Benachrichtigungen, um eine Vorlage auszuwählen",
|
||||
|
||||
// ServiceTypeField.tsx
|
||||
serviceType: "Diensttyp",
|
||||
serviceTypeHTTPDesc: "Webseiten und REST-APIs mit HTTP/HTTPS-Protokoll überwachen",
|
||||
serviceTypePINGDesc: "Host-Verfügbarkeit mit PING-Protokoll überwachen",
|
||||
serviceTypeTCPDesc: "TCP-Port-Konnektivität mit TCP-Protokoll überwachen",
|
||||
serviceTypeDNSDesc: "DNS-Auflösung überwachen",
|
||||
|
||||
// ServiceRegionalFields.tsx
|
||||
regionalMonitoring: "Regionale Überwachung",
|
||||
regionalMonitoringDesc: "Diesen Dienst regionalen Überwachungsagenten für verteilte Überwachung zuweisen",
|
||||
regionalAgents: "Regionale Agenten",
|
||||
regionalAgentsLoading: "Lade Agenten...",
|
||||
regionalAgentsAvailablePlaceholder: "Zusätzliche regionale Agenten auswählen...",
|
||||
regionalAgentsAllSelected: "Alle verfügbaren Agenten ausgewählt",
|
||||
regionalAgentsNoAvailable: "Keine regionalen Agenten verfügbar",
|
||||
regionalAgentsNoOnlineAvailable: "Keine regionalen Agenten online verfügbar",
|
||||
regionalAgentsNotFoundMessage: "Keine regionalen Online-Agenten gefunden. Dienste verwenden die Standardüberwachung.",
|
||||
regionalAgentsNotSelectedMessage: "Keine regionalen Agenten ausgewählt. Der Dienst verwendet die Standardüberwachung.",
|
||||
|
||||
// ServiceUrlField.tsx
|
||||
targetDefault: "Ziel-URL/Host",
|
||||
targetDNS: "Domainname",
|
||||
targetHTTPDesc: "Geben Sie die vollständige URL inklusive Protokoll ein (http:// oder https://)",
|
||||
targetPINGDesc: "Geben Sie den Hostnamen oder die IP-Adresse zum Pingen ein",
|
||||
targetTCPDesc: "Geben Sie Hostname oder IP-Adresse für TCP-Verbindungstest ein",
|
||||
targetTCPPortDesc: "Geben Sie die Portnummer für den TCP-Verbindungstest ein",
|
||||
targetDNSDesc: "Domainnamen für DNS-Überwachung eingeben (A, AAAA, MX, etc.)",
|
||||
targetDefaultDesc: "Geben Sie die Ziel-URL oder den Hostnamen für die Überwachung ein",
|
||||
targetDefaultPlaceholder: "URL oder Hostname eingeben",
|
||||
|
||||
// types.ts
|
||||
serviceNameRequired: "Dienstname ist erforderlich",
|
||||
urlDomainHostRequired: "URL/Domain/Host ist erforderlich",
|
||||
enterValidUrlHostnameDomain: "Bitte eine gültige URL, einen Hostnamen oder eine Domain eingeben",
|
||||
|
||||
// Dashboard
|
||||
upServices: "DIENSTE ONLINE",
|
||||
downServices: "DIENSTE OFFLINE",
|
||||
pausedServices: "DIENSTE PAUSIERT",
|
||||
warningServices: "DIENSTE MIT WARNUNG",
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
enableTLS: "TLS aktivieren",
|
||||
localName: "Lokaler Name",
|
||||
|
||||
// Test Email
|
||||
// Test Email
|
||||
testEmail: "Test Email",
|
||||
sendTestEmail: "Send test email",
|
||||
emailTemplate: "Email template",
|
||||
@@ -47,5 +47,12 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
testConnection: "Verbindung testen",
|
||||
testingConnection: "Verbindung wird getestet...",
|
||||
connectionSuccess: "Verbindung erfolgreich",
|
||||
connectionFailed: "Verbindung fehlgeschlagen"
|
||||
connectionFailed: "Verbindung fehlgeschlagen",
|
||||
|
||||
// Ergänzte fehlende Einträge
|
||||
addUser: "Benutzer hinzufügen",
|
||||
permissionNotice: "Berechtigungshinweis:",
|
||||
permissionNoticeAddUser: "Als Admin-Benutzer haben Sie keinen Zugriff auf die Anzeige oder Änderung von System- und E-Mail-Einstellungen. Diese Einstellungen können nur von Super-Admins aufgerufen und geändert werden. Wenden Sie sich an Ihren Super-Admin, wenn Sie Änderungen an der Systemkonfiguration oder den E-Mail-Einstellungen vornehmen müssen.",
|
||||
loadingSettings: "Einstellungen werden geladen...",
|
||||
loadingSettingsError: "Fehler beim Laden der Einstellungen",
|
||||
};
|
||||
|
||||
@@ -112,6 +112,7 @@ export const sslTranslations: SSLTranslations = {
|
||||
created: "Erstellt",
|
||||
lastUpdated: "Zuletzt aktualisiert",
|
||||
lastNotification: "Letzte Benachrichtigung",
|
||||
collectionId: "Sammlungs-ID"
|
||||
collectionId: "Sammlungs-ID",
|
||||
noCertificatesFound: "Keine Zertifikate gefunden",
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { AboutTranslations } from '../types/about';
|
||||
|
||||
export const aboutTranslations: AboutTranslations = {
|
||||
aboutCheckcle: "About Checkcle",
|
||||
aboutCheckCle: "About Checkcle",
|
||||
systemDescription: "Checkcle is an open-source monitoring stack offering real-time insights into server and service health, incident management, and operational transparency. Released as MIT License.",
|
||||
systemVersion: "System Version",
|
||||
license: "License",
|
||||
@@ -15,4 +15,20 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActions: "Quick Actions",
|
||||
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
|
||||
quickTips: "Quick Tips",
|
||||
releasedOn: "Released On",
|
||||
updateSchema: "Update Schema",
|
||||
updateSchemaDesc: "Automatic import collections schema",
|
||||
mergeFieldsLabel: "Merge fields with existing collections (safe - preserves data)",
|
||||
importing: "Importing...",
|
||||
clickToUpdateSchema: "Click to update Schema",
|
||||
importSuccessful: "Import Successful",
|
||||
importFailed: "Import Failed",
|
||||
instructions: "Instructions",
|
||||
mergeFields: "Merge fields",
|
||||
instructionsMergeFields: "Safely add new fields to existing collections, preserves all data",
|
||||
instructionsCollections: "System collections (starting with _) and users collection will be skipped automatically",
|
||||
instructionsImportAuth: "Only authenticated admins can perform schema imports",
|
||||
collectionsUpdatedCount: "{count} collections updated",
|
||||
collectionsCreatedCount: "{count} collections created",
|
||||
collectionsSkippedCount: "{count} collections created",
|
||||
};
|
||||
|
||||
@@ -5,10 +5,12 @@ export const commonTranslations: CommonTranslations = {
|
||||
welcome: "Welcome",
|
||||
logout: "Logout",
|
||||
language: "Language",
|
||||
english: "English",
|
||||
khmer: "Khmer",
|
||||
english: "Englisch",
|
||||
khmer: "ភាសាខ្មែរ",
|
||||
german: "Deutsch",
|
||||
simplifiedChinese: "Simplified Chinese",
|
||||
korean: "한국어",
|
||||
japanese: "日本語",
|
||||
simplifiedChinese: "简体中文",
|
||||
goodMorning: "Good morning",
|
||||
goodAfternoon: "Good afternoon",
|
||||
goodEvening: "Good evening",
|
||||
|
||||
@@ -52,4 +52,14 @@ export const incidentTranslations: IncidentTranslations = {
|
||||
configuration: 'Configuration',
|
||||
failedToUpdateStatus: 'Failed to update status',
|
||||
inProgress: 'In Progress',
|
||||
enterRootCause: 'Enter Root Cause',
|
||||
enterIncidentTitle: 'Enter Incident Title',
|
||||
enterIncidentDescription: 'Enter Incident Description',
|
||||
enterServiceId: 'Enter Service-IDs seperated by comma',
|
||||
selectAssignedUser: 'Select Assigned User',
|
||||
noAssignedUser: 'Incident is currently not assigned to any user',
|
||||
affectedSystems: 'Affected Systems',
|
||||
enterAffectedSystems: 'Enter Affected Systems',
|
||||
separateSystemsWithComma: 'Enter affected systems seperated by comma',
|
||||
createIncidentDesc: 'Create a new incident',
|
||||
};
|
||||
|
||||
@@ -65,4 +65,25 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
noScheduledMaintenance: 'No Scheduled Maintenance',
|
||||
noMaintenanceWindows: 'There are no maintenance windows for this period. Create one by clicking the "Create Maintenance" button.',
|
||||
maintenanceCreatedSuccess: 'Maintenance window created successfully',
|
||||
editMaintenanceWindow: 'Edit Maintenance',
|
||||
editMaintenanceDesc: 'Edit maintenance details',
|
||||
priorityDescription: 'Select maintenance priority',
|
||||
statusDescription: 'Select maintenance status',
|
||||
impactLevel: 'Impact Level',
|
||||
impactLevelDescription: 'Select impact level',
|
||||
selectAssignedUsers: 'Select Assigned Users',
|
||||
assignedPersonnelDescription: 'Currently assigned users',
|
||||
mutedNotifications: 'Notifications disabled',
|
||||
notificationsAreMuted: 'Notifications are currently muted for this maintenance',
|
||||
selectNotificationChannel: 'Add a notification channel',
|
||||
enableNotificationsFirst: 'Enable notifications first to select channel',
|
||||
updateMaintenance: 'Update Maintenance',
|
||||
loginToViewProfile: 'Please login to view your profile',
|
||||
goToLogin: 'Go to Login',
|
||||
loadingUserData: 'Loading user data...',
|
||||
retry: 'Retry',
|
||||
loadingServerData: 'Loading server data...',
|
||||
retrievingYourInformation: 'Please wait while we retrieve your information...',
|
||||
servicesPagination: '{startItem}-{endItem} of {totalItems} services',
|
||||
servicesPaginationNoService: '0 services',
|
||||
};
|
||||
|
||||
@@ -2,11 +2,138 @@
|
||||
import { ServicesTranslations } from '../types/services';
|
||||
|
||||
export const servicesTranslations: ServicesTranslations = {
|
||||
serviceName: "Service Name",
|
||||
serviceType: "Service Type",
|
||||
serviceStatus: "Service Status",
|
||||
responseTime: "Response Time",
|
||||
uptime: "Uptime",
|
||||
lastChecked: "Last Checked",
|
||||
noServices: "No services match your filter criteria.",
|
||||
};
|
||||
currentlyMonitoring: "Currently Monitoring",
|
||||
retry: "Retry",
|
||||
overview: "Overview",
|
||||
newService: "NewService",
|
||||
rowsPerPage: "Rows Per Page",
|
||||
search: "Search",
|
||||
allTypes: "All Types",
|
||||
createNewService: "Create New Service",
|
||||
createNewServiceDesc: "Fill in the details to create a new service to monitor.",
|
||||
|
||||
// ServiceBasicFields.tsx
|
||||
serviceName: "Service Name",
|
||||
serviceNameDesc: "Enter a descriptive name for your service",
|
||||
|
||||
// ServiceConfigFields.tsx
|
||||
checkInterval: "Check Interval",
|
||||
seconds: "seconds",
|
||||
minute: "minute",
|
||||
minutes: "minutes",
|
||||
hour: "hour",
|
||||
hours: "hours",
|
||||
custom: "Custom",
|
||||
checkIntervalPlaceholder: "Enter interval in seconds",
|
||||
backToPresets: "Back to presets",
|
||||
checkIntervalDesc: "How often to check the service status",
|
||||
checkIntervalDescCustom: "Enter custom interval in seconds (minimum 10 seconds)",
|
||||
retryAttempts: "Retry Attempts",
|
||||
attempt: "attempt",
|
||||
attempts: "attempts",
|
||||
retryAttemptsDesc: "Number of retry attempts before marking as down",
|
||||
|
||||
// ServiceForm.tsx
|
||||
updateService: "Update Service",
|
||||
createService: "Create Service",
|
||||
|
||||
// ServiceNotificationFields.tsx
|
||||
enableNotifications: "Enable Notifications",
|
||||
enableNotificationsDesc: "Enable or disable notifications for this service",
|
||||
notificationChannels: "Notification Channels",
|
||||
notificationChannelsEnabledDesc: "Select notification channels for this service",
|
||||
notificationChannelsDesc: "Enable notifications first to select channels",
|
||||
notificationChannelsPlaceholder: "Add a notification channel",
|
||||
alertTemplate: "Alert Template",
|
||||
alertTemplateLoading: "Loading templates...",
|
||||
alertTemplatePlaceholder: "Select an alert template",
|
||||
alertTemplateEnabledDesc: "Choose a template for alert messages",
|
||||
alertTemplateDesc: "Enable notifications first to select template",
|
||||
|
||||
// ServiceTypeField.tsx
|
||||
serviceType: "Service Type",
|
||||
serviceTypeHTTPDesc: "Monitor websites and REST APIs with HTTP/HTTPS Protocol",
|
||||
serviceTypePINGDesc: "Monitor host availability with PING Protocol",
|
||||
serviceTypeTCPDesc: "Monitor TCP port connectivity with TCP Protocol",
|
||||
serviceTypeDNSDesc: "Monitor DNS resolution",
|
||||
|
||||
// ServiceRegionalFields.tsx
|
||||
regionalMonitoring: "Regional Monitoring",
|
||||
regionalMonitoringDesc: "Assign this service to regional monitoring agents for distributed monitoring",
|
||||
regionalAgents: "Regional Agents",
|
||||
regionalAgentsLoading: "Loading agents...",
|
||||
regionalAgentsAvailablePlaceholder: "Select additional regional agents...",
|
||||
regionalAgentsAllSelected: "All available agents selected",
|
||||
regionalAgentsNoAvailable: "No regional agents available",
|
||||
regionalAgentsNoOnlineAvailable: "No online regional agents available",
|
||||
regionalAgentsNotFoundMessage: "No online regional agents found. Services will use default monitoring.",
|
||||
regionalAgentsNotSelectedMessage: "No regional agents selected. Service will use default monitoring.",
|
||||
|
||||
// ServiceUrlField.tsx
|
||||
targetDefault: "Target URL/Host",
|
||||
targetDNS: "Domain Name",
|
||||
targetHTTPDesc: "Enter the full URL including protocol (http:// or https://)",
|
||||
targetPINGDesc: "Enter hostname or IP address to ping",
|
||||
targetTCPDesc: "Enter hostname or IP address for TCP connection test",
|
||||
targetTCPPortDesc: "Enter the port number for TCP connection test",
|
||||
targetDNSDesc: "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)",
|
||||
targetDefaultDesc: "Enter the target URL or hostname for monitoring",
|
||||
targetDefaultPlaceholder: "Enter URL or hostname",
|
||||
|
||||
// types.ts
|
||||
serviceNameRequired: "Service name is required",
|
||||
urlDomainHostRequired: "URL/Domain/Host is required",
|
||||
enterValidUrlHostnameDomain: "Please enter a valid URL, hostname, or domain",
|
||||
|
||||
// Dashboard
|
||||
upServices: "UP SERVICES",
|
||||
downServices: "DOWN SERVICES",
|
||||
pausedServices: "PAUSED SERVICES",
|
||||
warningServices: "WARNING SERVICES",
|
||||
|
||||
// ServiceRowActions.tsx
|
||||
viewDetail: "View Detail",
|
||||
resumeMonitoring: "Resume Monitoring",
|
||||
pauseMonitoring: "Pause Monitoring",
|
||||
|
||||
|
||||
//IncidentTable.tsx
|
||||
responseTime: "Response Time",
|
||||
errorMessage: "Error Message",
|
||||
details: "Details",
|
||||
unmuteAlerts: "Unmute Alerts",
|
||||
muteAlerts: "Mute Alerts",
|
||||
|
||||
//LastCheckedTime.tsx
|
||||
pausedAt: "Paused at ",
|
||||
lastCheckDetails: "Last Check Details",
|
||||
checkedAt: "Checked at ",
|
||||
monitoringPaused: "Monitoring Paused",
|
||||
noAutomaticChecks: "No automatic checks",
|
||||
|
||||
//ServiveEditDialog.tsx
|
||||
editService: "Edit Service",
|
||||
editServiceDesc: "Update the details of your monitored service.",
|
||||
|
||||
//ServiceStatsCards.tsx
|
||||
lastCheckedAt: "Last checked at {datetime}",
|
||||
avg: "Avg",
|
||||
lastUpChecksCount: "last {count} up checks",
|
||||
basedOnlastChecksCount: "Based on last {count} checks",
|
||||
totalUptime: "Total Uptime",
|
||||
totalDowntime: "Total Downtime",
|
||||
monitoringSettings: "Monitoring Settings",
|
||||
monitoringSettingsInterval: "Checked every {interval} seconds",
|
||||
monitoringSettingsType: "monitoring",
|
||||
upStatusDuration: "Up for {duration}",
|
||||
downStatusDuration: "Down for {duration}",
|
||||
|
||||
incidentHistory: "Incident History",
|
||||
processing: "Processing",
|
||||
back: "Back",
|
||||
last20Checks: "Last 20 checks",
|
||||
};
|
||||
@@ -2,18 +2,18 @@
|
||||
import { SettingsTranslations } from '../types/settings';
|
||||
|
||||
export const settingsTranslations: SettingsTranslations = {
|
||||
// Tabs
|
||||
// General Settings - Tabs
|
||||
systemSettings: "System Settings",
|
||||
mailSettings: "Mail Settings",
|
||||
|
||||
// System Settings
|
||||
// General Settings - System Settings
|
||||
appName: "Application Name",
|
||||
appURL: "Application URL",
|
||||
senderName: "Sender Name",
|
||||
senderEmail: "Sender Email Address",
|
||||
hideControls: "Hide Controls",
|
||||
|
||||
// Mail Settings
|
||||
// General Settings - Mail Settings
|
||||
smtpSettings: "SMTP Configuration",
|
||||
smtpEnabled: "Enable SMTP",
|
||||
smtpHost: "SMTP Host",
|
||||
@@ -24,7 +24,7 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
enableTLS: "Enable TLS",
|
||||
localName: "Local Name",
|
||||
|
||||
// Test Email
|
||||
// General Settings - Test Email
|
||||
testEmail: "Test Email",
|
||||
sendTestEmail: "Send test email",
|
||||
emailTemplate: "Email template",
|
||||
@@ -37,9 +37,13 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
selectCollection: "Select collection",
|
||||
toEmailAddress: "To email address",
|
||||
enterEmailAddress: "Enter email address",
|
||||
send: "Send",
|
||||
sending: "Sending...",
|
||||
|
||||
// Actions and status
|
||||
testEmailSettings: "Test Email Settings",
|
||||
testEmailDescription: "Test whether the current Email Settings are available",
|
||||
testEmailAlert: "This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.",
|
||||
|
||||
// General Settings - Actions and status
|
||||
save: "Save Changes",
|
||||
saving: "Saving...",
|
||||
settingsUpdated: "Settings updated successfully",
|
||||
@@ -48,5 +52,12 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
testConnection: "Test Connection",
|
||||
testingConnection: "Testing Connection...",
|
||||
connectionSuccess: "Connection successful",
|
||||
connectionFailed: "Connection failed"
|
||||
connectionFailed: "Connection failed",
|
||||
|
||||
// User Management
|
||||
addUser: "Add User",
|
||||
permissionNotice: "Permission Notice:",
|
||||
permissionNoticeAddUser: "As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.",
|
||||
loadingSettings: "Loading settings...",
|
||||
loadingSettingsError: "Error loading settings",
|
||||
};
|
||||
@@ -112,5 +112,6 @@ export const sslTranslations: SSLTranslations = {
|
||||
created: "Created",
|
||||
lastUpdated: "Last Updated",
|
||||
lastNotification: "Last Notification",
|
||||
collectionId: "Collection ID"
|
||||
collectionId: "Collection ID",
|
||||
noCertificatesFound: "No Certificates Found",
|
||||
};
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import enTranslations from './en';
|
||||
import kmTranslations from './km';
|
||||
import deTranslations from './de';
|
||||
import koTranslations from './ko';
|
||||
import jaTranslations from './ja';
|
||||
import zhcnTranslations from './zhcn';
|
||||
|
||||
export type Language = "en" | "km" | "de" | "ja" | "zhcn";
|
||||
export type Language = "en" | "km" | "de" | "ko" | "ja" | "zhcn";
|
||||
|
||||
export const translations = {
|
||||
en: enTranslations,
|
||||
km: kmTranslations,
|
||||
de: deTranslations,
|
||||
ko: koTranslations,
|
||||
ja: jaTranslations,
|
||||
zhcn: zhcnTranslations,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AboutTranslations } from '../types/about';
|
||||
|
||||
export const aboutTranslations: AboutTranslations = {
|
||||
aboutCheckcle: "Checkcleについて",
|
||||
aboutCheckCle: "Checkcleについて",
|
||||
systemDescription: "Checkcleは、サーバーとサービスの健全性に関するリアルタイム監視、インシデント管理、運用の透明性を提供するオープンソースの監視スタックです。MIT ライセンスの下で公開されています。",
|
||||
systemVersion: "システムバージョン",
|
||||
license: "ライセンス",
|
||||
@@ -14,4 +14,5 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActions: "クイックアクション",
|
||||
quickActionsDescription: "一般的な監視操作と機能に素早くアクセスできます。開始するには、以下のアクションを選択してください。",
|
||||
quickTips: "クイックヒント",
|
||||
releasedOn: "公開日",
|
||||
};
|
||||
@@ -4,11 +4,12 @@ export const commonTranslations: CommonTranslations = {
|
||||
welcome: "ようこそ",
|
||||
logout: "ログアウト",
|
||||
language: "言語",
|
||||
english: "English",
|
||||
khmer: "Khmer",
|
||||
english: "Englisch",
|
||||
khmer: "ភាសាខ្មែរ",
|
||||
german: "Deutsch",
|
||||
simplifiedChinese: "简体中文",
|
||||
korean: "한국어",
|
||||
japanese: "日本語",
|
||||
simplifiedChinese: "简体中文",
|
||||
goodMorning: "おはようございます",
|
||||
goodAfternoon: "こんにちは",
|
||||
goodEvening: "こんばんは",
|
||||
|
||||
@@ -51,4 +51,5 @@ export const incidentTranslations: IncidentTranslations = {
|
||||
configuration: '設定',
|
||||
failedToUpdateStatus: 'ステータスの更新に失敗しました',
|
||||
inProgress: '進行中',
|
||||
enterRootCause: '原因を入力してください',
|
||||
};
|
||||
@@ -2,10 +2,20 @@ import { ServicesTranslations } from '../types/services';
|
||||
|
||||
export const servicesTranslations: ServicesTranslations = {
|
||||
serviceName: "サービス名",
|
||||
serviceNameDesc: "サービスのわかりやすい名前を入力してください",
|
||||
serviceType: "サービスタイプ",
|
||||
serviceStatus: "サービスステータス",
|
||||
responseTime: "応答時間",
|
||||
uptime: "稼働時間",
|
||||
lastChecked: "最終チェック",
|
||||
noServices: "フィルタ条件に一致するサービスがありません。",
|
||||
currentlyMonitoring: "現在監視中",
|
||||
retry: "再試行",
|
||||
overview: "概要",
|
||||
newService: "新しいサービス",
|
||||
rowsPerPage: "1ページあたりの行数",
|
||||
search: "検索",
|
||||
allTypes: "すべてのタイプ",
|
||||
createNewService: "新しいサービスを作成",
|
||||
createNewServiceDesc: "監視する新しいサービスを作成するには、詳細を入力してください。",
|
||||
};
|
||||
@@ -111,5 +111,6 @@ export const sslTranslations: SSLTranslations = {
|
||||
created: "作成日",
|
||||
lastUpdated: "最終更新",
|
||||
lastNotification: "最終通知",
|
||||
collectionId: "コレクションID"
|
||||
collectionId: "コレクションID",
|
||||
noCertificatesFound: "証明書が見つかりません",
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
import { AboutTranslations } from '../types/about';
|
||||
|
||||
export const aboutTranslations: AboutTranslations = {
|
||||
aboutCheckcle: "អំពី Checkcle",
|
||||
aboutCheckCle: "អំពី Checkcle",
|
||||
systemDescription: "Checkcle គឺជាស្តាកការត្រួតពិនិត្យប្រភពបើកទូលាយដែលផ្តល់នូវការយល់ដឹងជាក់ស្តែងអំពីសុខភាពម៉ាស៊ីនមេ និងសេវាកម្ម, ការគ្រប់គ្រងឧបទ្ទវហេតុ, និងតម្លាភាពនៃប្រតិបត្តិការ។ ចេញផ្សាយជាអាជ្ញាបណ្ណ MIT។",
|
||||
systemVersion: "កំណែប្រព័ន្ធ",
|
||||
license: "អាជ្ញាបណ្ណ",
|
||||
@@ -15,4 +15,5 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActions: "សកម្មភាពរហ័ស",
|
||||
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
|
||||
quickTips: "គន្លឹះរហ័ស",
|
||||
releasedOn: "បានចេញផ្សាយនៅថ្ងៃទី",
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user