27 Commits

Author SHA1 Message Date
Tola Leng 72984f5da4 Merge pull request #79 from operacle/develop
feat: Add create instance monitoring agent and Add Uptime Heatmap to service detail page
2025-07-22 16:08:14 +07:00
Tola Leng cc132a66d9 Merge pull request #70 from operacle/develop
- Improve service listing to display all records correctly
- Fetch all data and handle pagination for better UX
- Improve SSL Certs listing to display all records correctly
- Added ISSUE TEMPLATE 
- Added CODE OF CONDUCT
2025-07-15 22:18:48 +07:00
Tola Leng 3f975db568 Merge pull request #68 from operacle/develop
- feat: Implement notification channels based on status (Service dialog form) and refactor Service form into smaller components
- feat: Allow multiple selections for regional agents and support multiple notifications in the Service form
- feat: Add Japanese language support. Thank you @gnsworks for the contribution!
2025-07-13 18:44:44 +07:00
Tola Leng 547a4c4041 Merge pull request #65 from operacle/develop
feat: Implement notification channel based on status(multi selection) and Refactor Split ServiceForm into smaller components.
2025-07-10 22:17:02 +07:00
Tola Leng e29f60447d Merge pull request #64 from operacle/develop
feat: Improve server overview cards and fix regional_status to Service type
2025-07-08 22:03:15 +07:00
Tola Leng ad869c17aa add codeowners and pull request workflow 2025-07-03 21:03:09 +07:00
Tola Leng 6702bfa5e3 Merge pull request #62 from operacle/develop
Fix: Instance monitoring dashboard styling and Improved docker compose configuration
2025-07-03 20:59:59 +07:00
Tola Leng cb6946b7dd Create FUNDING.yml 2025-07-03 20:56:11 +07:00
Tola Leng 8bd8ae25e6 Merge pull request #61 from operacle/develop
Fix: Server Historical Performance Metrics date filter and improved the container entrypoint script pipeline
2025-07-02 20:05:50 +07:00
Tola Leng eddc12018f Merge pull request #60 from operacle/develop
Fix: Server history chart and date filter and Added Distributed Regional Monitoring Dockerfile
2025-07-01 20:07:23 +07:00
Tola Leng 474a439515 Merge pull request #59 from operacle/develop
feat: Implement instance and docker monitoring dashboard and detail metrics page
2025-06-30 23:07:58 +07:00
Tola Leng 9a4cc7c3db Merge pull request #57 from operacle/develop
Refactor: Separate data display by monitoring source and feat: Implement "Regional Monitoring" option
2025-06-26 16:28:32 +07:00
Tola Leng 23f3c5b8ca Merge pull request #53 from operacle/develop
Integrate regional agents in service-operation and Enhance Regional Agent Installation
2025-06-21 21:49:19 +07:00
Tola Leng f88cc32cf9 Merge pull request #52 from operacle/develop
Implement SSL certificate checker in Go instead of TS and feat: Add error message and details columns
2025-06-20 20:24:55 +07:00
Tola Leng 9f4e759b01 Merge pull request #51 from operacle/develop
Implement CheckCle Microservice Operation
2025-06-19 17:51:39 +07:00
Tola Leng 9ed4d4c7ac Merge pull request #50 from operacle/develop
feat: New Create Service Form and  Split UptimeBar.tsx into smaller components
2025-06-19 15:04:07 +07:00
Tola Leng 9a001f6aec Merge pull request #49 from operacle/develop
Remove HTTP service check and Refactor: Split settings API into modules
2025-06-18 22:09:40 +07:00
Tola Leng cfdedb9636 Merge pull request #45 from operacle/develop
Fix: Implement public status page functionality
2025-06-15 20:49:51 +07:00
Tola Leng 9d5fa805c9 Merge pull request #42 from operacle/develop
Implement password reset functionality.
2025-06-12 17:15:08 +07:00
Tola Leng 68a4ad5478 Merge pull request #38 from operacle/develop
Refactor: Split Sidebar.tsx into smaller components
2025-06-08 21:46:27 +07:00
Tola Leng 9b1918e25a Merge pull request #36 from operacle/develop
feat: Implement Operational and Public Status Page Feature
2025-06-06 19:23:54 +07:00
Tola Leng 1dc25cc461 Merge pull request #35 from operacle/develop
feat: Add mail test functionality
2025-06-06 16:48:46 +07:00
Tola Leng 4dc005c7e4 Merge pull request #33 from operacle/develop
Add SMTP password field in Mail Settings and  Fix: Ensure Add Service dialog respects theme
2025-06-05 17:40:17 +07:00
Tola Leng 4ec0c7a8fa Merge pull request #32 from operacle/develop
Fix: Improve email change flow and auto-logout and feat: Implement uptime monitoring data retention
2025-06-04 16:33:42 +07:00
Tola Leng dd758b9334 Merge pull request #27 from operacle/develop
feat: Implement uptime monitoring data retention
2025-05-31 17:44:43 +07:00
Tola Leng aec7118496 Merge pull request #25 from operacle/develop
feat: Auto logout after password change
2025-05-28 15:37:53 +07:00
Tola Leng 759c3249e1 Merge pull request #22 from operacle/develop
feat: release maintenance and incident management features; apply hotfixes
2025-05-26 21:57:45 +07:00
894 changed files with 7775 additions and 187102 deletions
-10
View File
@@ -1,10 +0,0 @@
.git
server/
**/node_modules
**/dist
**/.next
**/.turbo
**/.cache
**/coverage
**/*.log
+1 -1
View File
@@ -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
@@ -1,5 +1,5 @@
name: ❓ Ask for Help
description: Ask a question or request guidance related to this project.
description: Ask a question or request guidance about this project.
title: "[Question]: "
labels: [question]
assignees: []
@@ -8,8 +8,8 @@ body:
- type: markdown
attributes:
value: |
**Thanks for reaching out!**
Please provide as much detail as possible so we can better assist you.
**Thank you for reaching out!**
Please fill out this form to help us understand your question.
- type: textarea
id: question
@@ -17,17 +17,17 @@ body:
label: What do you need help with?
description: Clearly describe your question or what you're trying to achieve.
placeholder: |
I am trying to do X, but I'm not sure how to...
I am trying to do X but I'm not sure how to...
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional Context
label: Context
description: |
Provide any relevant context, related issues, pull requests, or documentation links that could help us understand your question better.
placeholder: "Mention related issues, links to docs, or previous discussions..."
Provide any additional context, related issues, or links that help explain your question.
placeholder: "Related issues, pull requests, or documentation..."
validations:
required: false
@@ -36,8 +36,8 @@ body:
attributes:
label: Environment Details
description: |
If relevant, provide details about your environment (e.g., OS, browser, Node.js version).
placeholder: "Example: Windows 11, Node.js v20.3.0, Firefox 125"
If applicable, please provide details about your environment (OS, Node version, browser, etc.).
placeholder: "Example: Ubuntu 22.04, Node.js v20, Chrome 125"
validations:
required: false
@@ -45,10 +45,9 @@ body:
id: checklist
attributes:
label: Checklist
description: |
Before submitting, please confirm the following:
description: Before submitting, please confirm:
options:
- label: I have searched existing issues and discussions for a solution.
- label: I have searched existing issues and discussions.
required: true
- label: I have read the relevant documentation.
- label: I have read the documentation.
required: true
+15 -15
View File
@@ -9,16 +9,16 @@ body:
attributes:
value: |
**Thank you for taking the time to report a bug!**
Please complete the form below to help us reproduce and fix the issue more effectively.
Please fill out the following template so we can reproduce and fix the issue faster.
- type: input
id: environment
attributes:
label: Environment
description: |
Describe the environment where the issue occurred.
(e.g., OS, browser, Node.js version, Docker version, etc.)
placeholder: "Example: Ubuntu 22.04, Chrome 125, Node.js 20.3.0"
Describe where you encountered the issue.
(e.g., OS, browser, Node version, etc.)
placeholder: "Example: macOS 14.0, Chrome 125, Node.js 20.3.0"
validations:
required: true
@@ -27,7 +27,7 @@ body:
attributes:
label: Bug Description
description: |
A clear and concise description of the issue you encountered.
A clear and concise description of what the bug is.
placeholder: "When I do X, Y happens instead of Z..."
validations:
required: true
@@ -37,12 +37,13 @@ body:
attributes:
label: Steps to Reproduce
description: |
Provide step-by-step instructions to reproduce the issue.
How can we reproduce the behavior?
Please list the steps in order.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See the error
4. See error
validations:
required: true
@@ -60,8 +61,8 @@ body:
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or screen recordings to help illustrate the issue.
placeholder: "Drag and drop or paste screenshots here."
If applicable, add screenshots or screen recordings to help explain your problem.
placeholder: "Attach screenshots here."
validations:
required: false
@@ -70,8 +71,8 @@ body:
attributes:
label: Logs
description: |
If applicable, paste any relevant logs or error messages.
Please remove sensitive information before submitting.
If applicable, paste logs or error messages here.
Please remove any sensitive information.
render: shell
placeholder: |
```
@@ -85,10 +86,9 @@ body:
id: terms
attributes:
label: Checklist
description: |
Please confirm the following before submitting:
description: Before submitting, please confirm:
options:
- label: I have searched existing issues to avoid creating duplicates.
- label: I have searched existing issues to avoid duplicates.
required: true
- label: I have provided sufficient information to reproduce the issue.
- label: I have provided enough information for reproduction.
required: true
+1 -1
View File
@@ -1 +1 @@
blank_issues_enabled: true
blank_issues_enabled: false
+9 -6
View File
@@ -15,7 +15,8 @@ body:
id: description
attributes:
label: Feature Description
description: A clear and concise description of the feature youd like to see.
description: |
A clear and concise description of the feature youd like to see.
placeholder: "I would like to have..."
validations:
required: true
@@ -24,7 +25,8 @@ body:
id: motivation
attributes:
label: Motivation
description: Please explain why this feature would be useful.
description: |
Please explain why this feature would be useful.
placeholder: "This feature would help because..."
validations:
required: true
@@ -33,7 +35,8 @@ body:
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
description: |
Have you considered any alternative solutions or workarounds?
placeholder: "I have tried..."
validations:
required: false
@@ -42,7 +45,8 @@ body:
id: additional
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
description: |
Add any other context or screenshots about the feature request here.
placeholder: "Links to related issues or references..."
validations:
required: false
@@ -51,8 +55,7 @@ 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 hasnt been requested yet.
required: true
@@ -1,59 +0,0 @@
name: Build and Publish Frontend Image
on:
push:
branches: [ "develop", "feature/**" ]
paths:
- 'application/**'
- '.github/workflows/build-frontend-image.yml'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/checkcle-frontend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha
- name: Build and push
uses: docker/build-push-action@v6
with:
context: application
file: application/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
-2
View File
@@ -22,8 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
pocketbase.exe
application/src/lib/pocketbase.ts
server/pb_data/
Dockerfile
application/src/lib/pocketbase-prod.ts
-4
View File
@@ -1,4 +0,0 @@
fileignoreconfig:
- filename: application/package-lock.json
checksum: 3253a06ef27a634ec2b8a8fe9c156288b9fcf57024623c7ddc3d321f2568e498
version: "1.0"
+2 -2
View File
@@ -79,7 +79,7 @@ Click "Fork" on [GitHub](https://github.com/operacle/checkcle) to create your ow
### 2. Clone Your Fork
```bash
git clone https://github.com/yourusername/checkcle.git
git clone --branch develop https://github.com/operacle/checkcle.git
cd checkcle
```
@@ -116,7 +116,7 @@ go run main.go (you do not need to change the .env while it's the localhost conn
Click "Fork" on [GitHub](https://github.com/operacle/Distributed-Regional-Monitoring) to create your own copy.
#### 2. Clone Your Fork
git clone https://github.com/yourusername/Distributed-Regional-Monitoring.git
git clone --branch main https://github.com/operacle/Distributed-Regional-Monitoring.git
cd Distributed-Regional-Monitoring
#### 3. Install Go Service (make sure you have installed the Go Service)
+62 -74
View File
@@ -20,12 +20,6 @@
<br/><strong>Japanese</strong>
</a>
</td>
<td align="center">
<a href="docs/README_zhcn.md">
<img src="https://flagcdn.com/24x18/cn.png" alt="Chinese" />
<br/><strong>Chinese</strong>
</a>
</td>
</tr>
</table>
@@ -45,10 +39,8 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
## 🎯 Live Demo
👉 **Try it now:** [CheckCle Live Demo](https://demo.checkcle.io)
User: admin@example.com | Passwd: Admin123456
## 🌟 Core Features
### 📝 Roadmap : [DEVELOPMENT_ROADMAP](docs/DEVELOPMENT_ROADMAP.md)
### Uptime Services & Infrastructure Server Monitoring
- Monitor HTTP, DNS, and Ping protocols
@@ -60,7 +52,7 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
- Infrastructure Server Monitoring, Supports Linux (🐧 Debian, Ubuntu, CentOS, Red Hat, etc.) and Windows (Beta). And Servers metrics like CPU, RAM, disk usage, and network activity) with an one-line installation angent script.
- Schedule Maintenance & Incident Management
- Operational Status / Public Status Pages
- Notifications via email, Telegram, Discord, Slack, Matrix, and more
- Notifications via email, Telegram, Discord, and Slack
- Reports & Analytics
- Settings Panel (User Management, Data Retention, Multi-language, Themes (Dark & Light Mode), Notification and channels and alert templates).
@@ -70,9 +62,26 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
* ✅ x86_64 PCs, laptops, servers (amd64)
* ✅ Modern Raspberry Pi 3/4/5 with (64-bit OS), Apple Silicon Macs (arm64)
### Install CheckCle using one of the following methods:
### Install CheckCle using one of the options below.
1. Install with Docker Compose Configuration (Recommended)
1. CheckCle One-Click Installation - Just copy and run on terminal
```bash
curl -fsSL https://checkcle.io/install.sh | bash
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Install with Docker compose Configuration.
```bash
version: '3.9'
@@ -92,18 +101,6 @@ services:
hard: 8192
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Admin Web Management
Default URL: http://0.0.0.0:8090
@@ -113,12 +110,33 @@ docker run -d \
4. Follow the Quick Start Guide at https://docs.checkcle.io
###
![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-1.4.png)
![Service Detail Page](https://cdn.checkcle.io/images/uptime/uptime-regional-detail.png)
![checkcle-server-instance](https://cdn.checkcle.io/images/server/server-list.png)
![SSL Monitoring](https://cdn.checkcle.io/images/ssl-domain/ssl-list.png)
![Notification System](https://cdn.checkcle.io/general/powerfull_notification.png)
![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-monitoring.png)
![Service Detail Page](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-service-detail.png)
![checkcle-server-instance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/instance-server-monitoring.png)
![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-schedule-maintenance.png)
![SSL Monitoring](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/ssl-monitoring.png)
## 📝 Development Roadmap
- ✅ Health check & uptime monitoring (HTTP)
- ✅ Dashboard UI with live stats
- ✅ Auth with Multi-users system (admin)
- ✅ Notifications (Telegram)
- ✅ Docker containerization
- ✅ CheckCle Website
- ✅ CheckCle Demo Server
- ✅ SSL & Domain Monitoring
- ✅ Schedule Maintenance
- ✅ Incident Management
- [ ] Infrastructure Server Monitoring
- ✅ Operational Status / Public Status Pages
- ✅ Uptime monitoring (HTTP, TCP, PING, DNS) Full functionality
- ✅ Distributed Regional Monitoring Agent [Support Operation](https://github.com/operacle/Distributed-Regional-Monitoring)
- ✅ System Setting Panel and Mail Settings
- ✅ User Permission Roles
- [ ] Notifications (Email/Slack/Discord/Signal)
- ✅ Data Retention & Automate Strink (Muti Options to Shrink Data & Database )
- ✅ Open-source release with full documentation
## 🌟 CheckCle for Communities?
- **Built with Passion**: Created by an open-source enthusiast for the community
@@ -127,48 +145,6 @@ docker run -d \
---
## Sponsors
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.
If youre a tech company interested in supporting CheckCle, please contact the author directly at tolaleng@checkcle.io
### 🤝 Ecosystem & Community Partner
<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>
</a>
<a href="https://www.jetbrains.com/">
<img src="https://cdn.checkcle.io/images/sponsor/jetbrains.png"
height="50"
alt="Jetbrains Logo" />
</a>
</div>
---
## 👥 Contributors
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
---
## 🤝 Ways to Contribute
Here are some ways you can help improve CheckCle:
@@ -177,18 +153,30 @@ Here are some ways you can help improve CheckCle:
- 🌟 **Suggest Features** Have an idea? Start a [Discussion](https://github.com/operacle/checkcle/discussions) or open a Feature Request issue.
- 🛠 **Submit Pull Requests** Improve the code, fix bugs, add features, or enhance the docs.
- 📝 **Improve Documentation** Even a typo fix helps!
- 🌍 **Spread the Word** Star ⭐ the [CheckCle](https://github.com/operacle/checkcle.git) repo, share it on socials, and invite others to contribute!
- 🌍 **Spread the Word** Star ⭐ the repo, share it on socials, and invite others to contribute!
---
## 🌍 Stay Connected
- Website: [checkcle.io](https://checkcle.io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle!
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- Follow us on X: [@asqrm_io](https://x.com/asqrm_io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
- GitHub Repository: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
- Community Channels: Engage via discussions and issues!
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
## 📜 License
CheckCle is released under the MIT License.
---
## 👥 Contributors
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=operacle/checkcle&type=Date)](https://www.star-history.com/#operacle/checkcle&Date)
Stay informed, stay online, with CheckCle! 🌐
-1
View File
@@ -39,7 +39,6 @@ We support the latest stable release of `checkcle`. Security patches may also be
| Version | Supported |
|---------|-----------|
| `develop` (latest) | ✅ Yes |
| `main` (latest) | ✅ Yes |
| Older versions | ⚠️ Best-effort |
| Pre-release or forks | ❌ No |
-6
View File
@@ -1,6 +0,0 @@
.git
node_modules
dist
.cache
*.log
-18
View File
@@ -1,18 +0,0 @@
# syntax=docker/dockerfile:1.7
FROM node:20-bullseye-slim AS build
WORKDIR /app
ENV npm_config_fund=false \
npm_config_audit=false \
npm_config_progress=false
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --fund=false
COPY . .
RUN --mount=type=cache,target=/root/.npm npm run build
FROM nginx:1.27-alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build /app/dist /usr/share/nginx/html
# SPA fallback + asset caching
RUN printf 'server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n location ~* \\.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; }\n}\n' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx","-g","daemon off;"]
+2 -2
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle</title>
<title>CheckCle is an open-source monitoring stack</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" />
@@ -14,7 +14,7 @@
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:site" content="@tlengoss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
+22 -404
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -50,7 +50,7 @@
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"pocketbase": "^0.26.8",
"pocketbase": "^0.19.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

@@ -7,7 +7,6 @@ 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[];
@@ -16,7 +15,6 @@ interface 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);
@@ -33,7 +31,7 @@ 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()}>{t('retry')}</Button>
<Button onClick={() => window.location.reload()}>Retry</Button>
</div>
);
}
@@ -42,12 +40,12 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground">{t('overview')}</h2>
<h2 className="text-2xl font-bold text-foreground">Overview</h2>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> {t('newService')}
<Plus className="w-4 h-4 mr-2" /> New Service
</Button>
</div>
+122 -98
View File
@@ -1,59 +1,78 @@
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, Menu, X
} from "lucide-react";
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut, Grid3x3 } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuSeparator
} from "@/components/ui/dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings";
import { useSidebar } from "@/contexts/SidebarContext";
import { cn } from "@/lib/utils";
import QuickActionsDialog from "./QuickActionsDialog";
interface HeaderProps {
currentUser: AuthUser | null;
onLogout: () => void;
sidebarCollapsed: boolean;
toggleSidebar: () => void;
}
export const Header = ({
currentUser,
onLogout,
sidebarCollapsed,
toggleSidebar
}: HeaderProps) => {
const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage();
const { sidebarCollapsed, toggleSidebar, isMobileOpen, toggleMobileMenu } = useSidebar();
const [greeting, setGreeting] = useState<string>("");
const { systemName } = useSystemSettings();
const navigate = useNavigate();
const [quickActionsOpen, setQuickActionsOpen] = useState(false);
// Set greeting based on time of day
useEffect(() => {
const updateGreeting = () => {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) setGreeting(t("goodMorning"));
else if (hour >= 12 && hour < 18) setGreeting(t("goodAfternoon"));
else setGreeting(t("goodEvening"));
if (hour >= 5 && hour < 12) {
setGreeting(t("goodMorning"));
} else if (hour >= 12 && hour < 18) {
setGreeting(t("goodAfternoon"));
} else {
setGreeting(t("goodEvening"));
}
};
updateGreeting();
// Update greeting if language changes
// You could add a timer to update the greeting throughout the day
// but that's typically unnecessary since most sessions aren't active across time periods
}, [language, t]);
const avatarUrl = currentUser?.avatar || '';
// Log avatar data for debugging
useEffect(() => {
if (currentUser) {
// console.log("Avatar URL in Header:", currentUser.avatar);
}
}, [currentUser]);
const openExternalLink = (url: string) => {
window.open(url, '_blank');
};
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
let avatarUrl = '';
if (currentUser?.avatar) {
// If it's a relative path from the public folder, make sure it's resolved properly
if (currentUser.avatar.startsWith('/upload/profile/')) {
avatarUrl = currentUser.avatar;
} else {
avatarUrl = currentUser.avatar;
}
// console.log("Final avatar URL:", avatarUrl);
}
return (
<header className="relative bg-background border-b border-border px-4 lg:px-6 flex justify-between items-center h-16 shrink-0 z-30 overflow-hidden">
{/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0 pointer-events-none opacity-20">
<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={{
@@ -66,74 +85,29 @@ export const Header = ({
</div>
</div>
<div className="flex items-center gap-2 lg:gap-4 z-10">
{/* Mobile Toggle */}
<Button variant="ghost" size="icon" onClick={toggleMobileMenu} className="lg:hidden h-9 w-9">
{isMobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
{/* Desktop Toggle */}
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="hidden lg:flex h-9 w-9">
{/* 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" />}
</Button>
<div className="flex items-center">
<h1 className="text-sm lg:text-lg font-medium truncate max-w-[150px] lg:max-w-none">
{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋
</h1>
{/* Quick Actions Button */}
<Button
variant="ghost"
size="icon"
onClick={() => setQuickActionsOpen(true)}
className="mr-2"
>
<Grid3x3 className="h-5 w-5 text-green-500" />
</Button>
<div className="flex items-center space-x-2">
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1>
</div>
</div>
<div className="flex items-center space-x-1 lg:space-x-3 z-10">
{/* External Links - Hidden on small screens */}
<div className="hidden md:flex items-center space-x-1 mr-2">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://docs.checkcle.io')}
title={t("documentation")}
>
<FileText className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://github.com/operacle/checkcle')}
title="GitHub"
>
<Github className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://x.com/checkcle_oss')}
title="X (Twitter)"
>
<Twitter className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://discord.gg/xs9gbubGwX')}
title="Discord"
>
<MessageSquare className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
title={t("notifications")}
>
<Bell className="w-4 h-4" />
</Button>
</div>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border hidden sm:flex" onClick={toggleTheme}>
<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}>
<span className="sr-only">Toggle theme</span>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</Button>
@@ -145,7 +119,7 @@ export const Header = ({
<Globe className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
{t("english")}
</DropdownMenuItem>
@@ -153,35 +127,82 @@ export const Header = ({
{t("khmer")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
{t("german")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
{t("korean")}
{t("Deutsch")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
{t("japanese")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
{t("simplifiedChinese")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="h-8 w-px bg-border mx-1 hidden sm:block" />
{/* Documentation */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
>
<span className="sr-only">{t("documentation")}</span>
<FileText className="w-4 h-4" />
</Button>
{/* GitHub */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
>
<span className="sr-only">GitHub</span>
<Github className="w-4 h-4" />
</Button>
{/* X (Twitter) */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/tlengoss", "_blank")}
>
<span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" />
</Button>
{/* Discord */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
>
<span className="sr-only">Discord</span>
<MessageSquare className="w-4 h-4" />
</Button>
{/* Notifications */}
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<span className="sr-only">{t("notifications")}</span>
<Bell className="w-4 h-4" />
</Button>
{/* User Profile Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="h-8 w-8 cursor-pointer border hover:ring-2 hover:ring-primary/20 transition-all">
{avatarUrl ? <AvatarImage src={avatarUrl} alt="User" /> : <AvatarFallback className="bg-primary/20 text-primary">{currentUser?.name?.[0] || 'U'}</AvatarFallback>}
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
</Avatar>
</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="User" /> : <AvatarFallback className="bg-primary/20 text-primary">{currentUser?.name?.[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-0.5">
<span className="text-sm font-medium truncate">{currentUser?.name || 'User'}</span>
<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>
</div>
</div>
@@ -202,6 +223,9 @@ export const Header = ({
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Quick Actions Dialog */}
<QuickActionsDialog isOpen={quickActionsOpen} setIsOpen={setQuickActionsOpen} />
</header>
);
};
@@ -0,0 +1,145 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogClose
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface QuickActionsDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
export const QuickActionsDialog: React.FC<QuickActionsDialogProps> = ({ isOpen, setIsOpen }) => {
const { t } = useLanguage();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[800px] bg-background max-h-[90vh] overflow-y-auto">
<DialogClose className="absolute right-4 top-4">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogClose>
<DialogHeader>
<DialogTitle className="text-xl font-bold">{t('quickActions')}</DialogTitle>
<DialogDescription>
{t('quickActionsDescription')}
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
{/* Website Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorWebsite')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorWebsiteDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorMultipleEndpoints')}</li>
<li>{t('trackResponseTimes')}</li>
<li>{t('setCustomAlerts')}</li>
</ul>
</div>
</div>
{/* Server Monitoring Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('monitorServer')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('monitorServerDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('monitorCPURam')}</li>
<li>{t('trackNetworkMetrics')}</li>
<li>{t('viewProcesses')}</li>
</ul>
</div>
</div>
{/* SSL Certificate Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('sslCertificate')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('sslCertificateDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('trackSSLExpiration')}</li>
<li>{t('getEarlyRenewal')}</li>
<li>{t('monitorMultipleDomains')}</li>
</ul>
</div>
</div>
{/* Incidents Management Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('incidentsManagement')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('incidentsManagementDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createTrackIncidents')}</li>
<li>{t('assignIncidents')}</li>
<li>{t('monitorResolution')}</li>
</ul>
</div>
</div>
{/* Operational Status Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('operationalStatus')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('operationalStatusDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('createCustomStatus')}</li>
<li>{t('displayRealTime')}</li>
<li>{t('shareIncidentUpdates')}</li>
</ul>
</div>
</div>
{/* Reports & Analytics Card */}
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
<h3 className="font-semibold text-lg mb-2">{t('reportsAnalytics')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('reportsAnalyticsDesc')}
</p>
<div className="space-y-2">
<p className="text-sm font-medium">{t('quickTips')}:</p>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>{t('generateUptimeReports')}</li>
<li>{t('analyzePerformance')}</li>
<li>{t('exportMonitoringData')}</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end mt-6">
<Button variant="outline" onClick={() => setIsOpen(false)}>
{t('close')}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default QuickActionsDialog;
@@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServiceFiltersProps {
filter: string;
@@ -20,11 +19,10 @@ export const ServiceFilters = ({
setSearchTerm,
servicesCount
}: ServiceFiltersProps) => {
const { t } = useLanguage();
return (
<div className="mb-6 flex justify-between items-center">
<div className="flex items-center">
<h3 className="text-xl font-semibold mr-2 text-foreground">{t('currentlyMonitoring')}</h3>
<h3 className="text-xl font-semibold mr-2 text-foreground">Currently Monitoring</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
{servicesCount}
</span>
@@ -32,10 +30,10 @@ export const ServiceFilters = ({
<div className="flex space-x-4">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-40 bg-card border-border">
<SelectValue placeholder={t('allTypes')} />
<SelectValue placeholder="All Types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('allTypes')}</SelectItem>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="HTTP">HTTP</SelectItem>
<SelectItem value="PING">PING</SelectItem>
<SelectItem value="TCP">TCP</SelectItem>
@@ -45,7 +43,7 @@ export const ServiceFilters = ({
<div className="relative">
<Input
className="w-72 bg-card border-border"
placeholder={t('search')}
placeholder="Search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -1,44 +1,28 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
import { SidebarHeader } from "./sidebar/SidebarHeader";
import { MainNavigation } from "./sidebar/MainNavigation";
import { SettingsPanel } from "./sidebar/SettingsPanel";
import { useSidebar } from "@/contexts/SidebarContext";
import { cn } from "@/lib/utils";
export const Sidebar = () => {
interface SidebarProps {
collapsed: boolean;
}
export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme();
const { sidebarCollapsed, isMobileOpen, toggleMobileMenu } = useSidebar();
return (
<>
{/* Mobile Overlay */}
{isMobileOpen && (
<div
className="fixed inset-0 bg-black/60 z-40 lg:hidden backdrop-blur-sm transition-opacity animate-in fade-in"
onClick={toggleMobileMenu}
/>
)}
{/* Sidebar Container */}
<aside
className={cn(
"fixed inset-y-0 left-0 z-50 lg:static lg:block transition-all duration-300 ease-in-out border-r flex flex-col h-full",
theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border',
// Mobile state
isMobileOpen ? "translate-x-0 w-64" : "-translate-x-full lg:translate-x-0",
// Desktop state (collapsed/expanded)
!isMobileOpen && sidebarCollapsed ? "lg:w-16" : "lg:w-64"
)}
>
<SidebarHeader collapsed={!isMobileOpen && sidebarCollapsed} />
<div className="flex-1 overflow-y-auto overflow-x-hidden custom-scrollbar">
<MainNavigation collapsed={!isMobileOpen && sidebarCollapsed} />
</div>
<SettingsPanel collapsed={!isMobileOpen && sidebarCollapsed} />
</aside>
</>
<div
className={`
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
border-r flex flex-col h-full
${collapsed ? 'w-16' : 'w-64'}
`}
>
<SidebarHeader collapsed={collapsed} />
<MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} />
</div>
);
};
@@ -3,15 +3,12 @@ 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;
@@ -45,7 +42,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("upServices")}</CardTitle>
<CardTitle className="text-sm font-medium text-white">UP SERVICES</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{upServices}</span>
@@ -77,7 +74,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("downServices")}</CardTitle>
<CardTitle className="text-sm font-medium text-white">DOWN SERVICES</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{downServices}</span>
@@ -109,7 +106,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("pausedServices")}</CardTitle>
<CardTitle className="text-sm font-medium text-white">PAUSED SERVICES</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{pausedServices}</span>
@@ -141,7 +138,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">{t("warningServices")}</CardTitle>
<CardTitle className="text-sm font-medium text-white">WARNING SERVICES</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{warningServices}</span>
@@ -1,9 +1,9 @@
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface MenuItemProps {
id: string;
@@ -16,6 +16,7 @@ interface MenuItemProps {
}
export const MenuItem: React.FC<MenuItemProps> = ({
id,
path,
icon: Icon,
translationKey,
@@ -28,39 +29,36 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const location = useLocation();
const navigate = useNavigate();
const isActive = path && location.pathname === path;
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (hasNavigation && path) {
navigate(path);
navigate(path, { replace: false });
}
};
const isActive = path && location.pathname === path;
const mainIconSize = "h-6 w-6";
return (
<div
className={cn(
"group relative flex items-center mb-1 rounded-lg cursor-pointer transition-all duration-200",
collapsed ? "p-3 justify-center" : "p-2.5 px-4",
isActive
? (theme === 'dark' ? 'bg-zinc-800 text-white' : 'bg-primary/10 text-primary')
: (theme === 'dark' ? 'text-zinc-400 hover:bg-zinc-800/50 hover:text-white' : 'text-zinc-600 hover:bg-zinc-100')
)}
className={`
${collapsed ? 'p-3' : 'p-2 pl-3'}
mb-1 rounded-lg
${isActive ? (theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent') : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`}
flex items-center
${collapsed ? 'justify-center' : ''}
cursor-pointer
`}
onClick={handleClick}
title={collapsed ? t(translationKey) : ""}
>
<Icon className={cn("h-5 w-5 shrink-0 transition-colors", color, isActive ? "opacity-100" : "opacity-80 group-hover:opacity-100")} />
<Icon className={`${mainIconSize} ${color}`} />
{!collapsed && (
<span className="ml-3 font-medium tracking-wide text-sm truncate">
<span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
{t(translationKey)}
</span>
)}
{/* Small Indicator for active state */}
{isActive && !collapsed && (
<div className="absolute left-0 w-1 h-6 bg-primary rounded-r-full" />
)}
</div>
);
};
@@ -4,7 +4,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody } from "@/components/ui/table";
import { DockerContainer } from "@/types/docker.types";
import { DockerMetricsDialog } from "./DockerMetricsDialog";
import { useLanguage } from "@/contexts/LanguageContext";
import {
DockerTableSearch,
DockerTableHeader,
@@ -19,7 +18,6 @@ interface DockerContainersTableProps {
}
export const DockerContainersTable = ({ containers, isLoading, onRefresh }: DockerContainersTableProps) => {
const { t } = useLanguage();
const [searchTerm, setSearchTerm] = useState("");
const [selectedContainer, setSelectedContainer] = useState<DockerContainer | null>(null);
const [metricsDialogOpen, setMetricsDialogOpen] = useState(false);
@@ -51,7 +49,7 @@ export const DockerContainersTable = ({ containers, isLoading, onRefresh }: Dock
<CardHeader className="pb-4 px-0">
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="text-lg sm:text-xl font-semibold">{t('dockerContainers', 'docker')}</CardTitle>
<CardTitle className="text-lg sm:text-xl font-semibold">Docker Containers</CardTitle>
<DockerTableSearch
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
@@ -10,7 +10,6 @@ import { DockerContainer, DockerMetrics } from "@/types/docker.types";
import { dockerService } from "@/services/dockerService";
import { Loader2, Cpu, HardDrive, Network, MemoryStick } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerMetricsDialogProps {
container: DockerContainer | null;
@@ -21,17 +20,16 @@ interface DockerMetricsDialogProps {
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
const timeRangeOptions = [
{ value: '60m' as TimeRange, label: 'minutes60', hours: 1 },
{ value: '1d' as TimeRange, label: 'day1', hours: 24 },
{ value: '7d' as TimeRange, label: 'days7', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: 'month1', hours: 24 * 30 },
{ value: '3m' as TimeRange, label: 'months3', hours: 24 * 90 },
{ value: '60m' as TimeRange, label: '60 minutes', hours: 1 },
{ value: '1d' as TimeRange, label: '1 day', hours: 24 },
{ value: '7d' as TimeRange, label: '7 days', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: '1 month', hours: 24 * 30 },
{ value: '3m' as TimeRange, label: '3 months', hours: 24 * 90 },
];
export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMetricsDialogProps) => {
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
const { theme } = useTheme();
const { t } = useLanguage();
const {
data: metrics = [],
@@ -234,11 +232,11 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<DialogContent className="max-w-7xl max-h-[95vh] overflow-y-auto bg-background text-foreground">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
<Cpu className="h-4 w-4 text-primary" />
</div>
{t('containerMetricsTitle', 'docker', { name: container.name })}
Container Metrics: {container.name}
</div>
<div className="flex items-center gap-2">
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
@@ -248,7 +246,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<SelectContent>
{timeRangeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(option.label, 'docker')}
{option.label}
</SelectItem>
))}
</SelectContent>
@@ -256,22 +254,22 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
</div>
</DialogTitle>
<p className="text-sm text-muted-foreground">
{t('dockerId', 'docker')}: {container.docker_id} {container.hostname}
Docker ID: {container.docker_id} {container.hostname}
</p>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center h-96">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">{t('loadingMetrics', 'docker')}</span>
<span className="ml-2">Loading metrics...</span>
</div>
) : error ? (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>{t('errorLoadingMetrics', 'docker')}: {String((error as any)?.message ?? '')}</p>
<p>Error loading metrics: {error.message}</p>
</div>
) : chartData.length === 0 ? (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>{t('noMetricsAvailable', 'docker')}</p>
<p>No metrics data available for this container</p>
</div>
) : (
<>
@@ -279,7 +277,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
{latestMetric && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<MetricCard
title={t('cpu', 'docker')}
title="CPU"
used={`${latestMetric.cpuUsage}%`}
total={`${latestMetric.cpuCores} cores`}
free={`${(100 - latestMetric.cpuUsage).toFixed(1)}%`}
@@ -288,7 +286,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
color={chartConfig.cpuUsage.color}
/>
<MetricCard
title={t('memoryTitle', 'docker')}
title="Memory"
used={latestMetric.ramUsed}
total={latestMetric.ramTotal}
free={latestMetric.ramFree}
@@ -297,7 +295,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
color={chartConfig.ramUsagePercent.color}
/>
<MetricCard
title={t('diskTitle', 'docker')}
title="Disk"
used={latestMetric.diskUsed}
total={latestMetric.diskTotal}
free={latestMetric.diskFree}
@@ -306,10 +304,10 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
color={chartConfig.diskUsagePercent.color}
/>
<MetricCard
title={t('network', 'docker')}
title="Network"
used={`RX: ${latestMetric.networkRx}`}
total={`TX: ${latestMetric.networkTx}`}
free={`${t('networkSpeedKbs', 'docker').replace('(KB/s)', '')}: ${latestMetric.networkRxSpeed} KB/s`}
free={`Speed: ${latestMetric.networkRxSpeed} KB/s`}
percentage={0}
icon={Network}
color={chartConfig.networkRx.color}
@@ -321,19 +319,19 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<TabsList className="grid w-full grid-cols-4 bg-muted">
<TabsTrigger value="cpu" className="flex items-center gap-2 data-[state=active]:bg-background">
<Cpu className="h-4 w-4" />
{t('cpu', 'docker')}
CPU
</TabsTrigger>
<TabsTrigger value="memory" className="flex items-center gap-2 data-[state=active]:bg-background">
<MemoryStick className="h-4 w-4" />
{t('memoryTitle', 'docker')}
Memory
</TabsTrigger>
<TabsTrigger value="disk" className="flex items-center gap-2 data-[state=active]:bg-background">
<HardDrive className="h-4 w-4" />
{t('diskTitle', 'docker')}
Disk
</TabsTrigger>
<TabsTrigger value="network" className="flex items-center gap-2 data-[state=active]:bg-background">
<Network className="h-4 w-4" />
{t('network', 'docker')}
Network
</TabsTrigger>
</TabsList>
@@ -341,7 +339,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('cpuUsagePct', 'docker')}</CardTitle>
<CardTitle className="text-foreground">CPU Usage (%)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -367,7 +365,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.cpuUsage.color}
strokeWidth={2}
dot={{ r: 3, fill: chartConfig.cpuUsage.color }}
name={t('cpuUsagePct', 'docker')}
name="CPU Usage (%)"
/>
</LineChart>
</ChartContainer>
@@ -376,7 +374,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('cpuUsageVsAvailable', 'docker')}</CardTitle>
<CardTitle className="text-foreground">CPU Usage vs Available</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -403,7 +401,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name={t('cpuAvailablePct', 'docker')}
name="CPU Available (%)"
/>
<Area
type="monotone"
@@ -412,7 +410,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.cpuUsage.color}
fill={chartConfig.cpuUsage.color}
fillOpacity={0.6}
name={t('cpuUsagePct', 'docker')}
name="CPU Usage (%)"
/>
</AreaChart>
</ChartContainer>
@@ -425,7 +423,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('ramUsagePct', 'docker')}</CardTitle>
<CardTitle className="text-foreground">Memory Usage (%)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -461,7 +459,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('memoryUsageBytes', 'docker')}</CardTitle>
<CardTitle className="text-foreground">Memory Usage (Bytes)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -481,8 +479,8 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
formatter={(value, name) => [
name === t('usedMemory', 'docker') ? formatBytes(Number(value)) :
name === t('totalMemory', 'docker') ? formatBytes(Number(value)) : value,
name === 'Used Memory' ? formatBytes(Number(value)) :
name === 'Total Memory' ? formatBytes(Number(value)) : value,
name
]}
/>
@@ -493,7 +491,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name={t('totalMemory', 'docker')}
name="Total Memory"
/>
<Area
type="monotone"
@@ -502,7 +500,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.ramUsagePercent.color}
fill={chartConfig.ramUsagePercent.color}
fillOpacity={0.6}
name={t('usedMemory', 'docker')}
name="Used Memory"
/>
</AreaChart>
</ChartContainer>
@@ -515,7 +513,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('diskUsagePct', 'docker')}</CardTitle>
<CardTitle className="text-foreground">Disk Usage (%)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -542,7 +540,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.diskUsagePercent.color}
fill={chartConfig.diskUsagePercent.color}
fillOpacity={0.6}
name={t('diskUsagePct', 'docker')}
name="Disk Usage (%)"
/>
</AreaChart>
</ChartContainer>
@@ -551,7 +549,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('diskUsageBytes', 'docker')}</CardTitle>
<CardTitle className="text-foreground">Disk Usage (Bytes)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -571,8 +569,8 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
formatter={(value, name) => [
name === t('usedDisk', 'docker') ? formatBytes(Number(value)) :
name === t('totalDisk', 'docker') ? formatBytes(Number(value)) : value,
name === 'Used Disk' ? formatBytes(Number(value)) :
name === 'Total Disk' ? formatBytes(Number(value)) : value,
name
]}
/>
@@ -583,7 +581,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name={t('totalDisk', 'docker')}
name="Total Disk"
/>
<Area
type="monotone"
@@ -592,7 +590,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.diskUsagePercent.color}
fill={chartConfig.diskUsagePercent.color}
fillOpacity={0.6}
name={t('usedDisk', 'docker')}
name="Used Disk"
/>
</AreaChart>
</ChartContainer>
@@ -605,7 +603,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('networkTraffic', 'docker')}</CardTitle>
<CardTitle className="text-foreground">Network Traffic</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-64">
@@ -629,7 +627,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkRxBytes"
stroke={chartConfig.networkRx.color}
strokeWidth={2}
name={t('rxBytes', 'docker')}
name="RX Bytes"
dot={{ r: 2 }}
/>
<Line
@@ -637,7 +635,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkTxBytes"
stroke={chartConfig.networkTx.color}
strokeWidth={2}
name={t('txBytes', 'docker')}
name="TX Bytes"
dot={{ r: 2 }}
/>
</LineChart>
@@ -647,7 +645,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">{t('networkSpeedKbs', 'docker')}</CardTitle>
<CardTitle className="text-foreground">Network Speed (KB/s)</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-64">
@@ -671,7 +669,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkRxSpeed"
stroke={chartConfig.networkRx.color}
strokeWidth={2}
name={t('rxSpeedKbs', 'docker')}
name="RX Speed (KB/s)"
dot={{ r: 2 }}
/>
<Line
@@ -679,7 +677,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkTxSpeed"
stroke={chartConfig.networkTx.color}
strokeWidth={2}
name={t('txSpeedKbs', 'docker')}
name="TX Speed (KB/s)"
dot={{ r: 2 }}
/>
</LineChart>
@@ -4,7 +4,6 @@ import { Badge } from "@/components/ui/badge";
import { Container, Play, Square, AlertTriangle } from "lucide-react";
import { DockerStats } from "@/types/docker.types";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerStatsCardsProps {
stats: DockerStats;
@@ -12,11 +11,10 @@ interface DockerStatsCardsProps {
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
const { theme } = useTheme();
const { t } = useLanguage();
const cards = [
{
title: t('totalContainers', 'docker'),
title: "Total Containers",
value: stats.total,
icon: Container,
color: "text-blue-600",
@@ -25,7 +23,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)"
},
{
title: t('running', 'docker'),
title: "Running",
value: stats.running,
icon: Play,
color: "text-green-600",
@@ -34,7 +32,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)"
},
{
title: t('stopped', 'docker'),
title: "Stopped",
value: stats.stopped,
icon: Square,
color: "text-gray-600",
@@ -43,7 +41,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)"
},
{
title: t('warning', 'docker'),
title: "Warning",
value: stats.warning,
icon: AlertTriangle,
color: "text-amber-600",
@@ -92,7 +90,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
variant="outline"
className="text-xs font-mono font-bold px-2 py-1 rounded-md bg-white/20 backdrop-blur-sm text-white border border-white/30"
>
{t('containersLabel', 'docker')}
Containers
</Badge>
</div>
</CardContent>
@@ -1,38 +1,36 @@
import { Badge } from "@/components/ui/badge";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerStatusBadgeProps {
status: 'running' | 'stopped' | 'warning';
}
export const DockerStatusBadge = ({ status }: DockerStatusBadgeProps) => {
const { t } = useLanguage();
const getStatusConfig = (status: string) => {
switch (status) {
case 'running':
return {
variant: 'default' as const,
className: 'bg-emerald-100 text-emerald-800 border-emerald-200 hover:bg-emerald-200',
label: t('running', 'docker')
label: 'Running'
};
case 'stopped':
return {
variant: 'secondary' as const,
className: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200',
label: t('stopped', 'docker')
label: 'Stopped'
};
case 'warning':
return {
variant: 'destructive' as const,
className: 'bg-amber-100 text-amber-800 border-amber-200 hover:bg-amber-200',
label: t('warning', 'docker')
label: 'Warning'
};
default:
return {
variant: 'outline' as const,
className: 'bg-gray-100 text-gray-600 border-gray-200',
label: t('unknown', 'docker')
label: 'Unknown'
};
}
};
@@ -1,22 +1,20 @@
import { TableCell, TableRow } from "@/components/ui/table";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerEmptyStateProps {
searchTerm: string;
}
export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => {
const { t } = useLanguage();
return (
<TableRow>
<TableCell colSpan={8} className="text-center py-12 text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<div className="text-lg font-medium">
{searchTerm ? t('noContainersFound', 'docker') : t('noContainersRunning', 'docker')}
{searchTerm ? "No containers found" : "No containers running"}
</div>
<div className="text-sm">
{searchTerm ? t('tryAdjustSearch', 'docker') : t('startSomeContainers', 'docker')}
{searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."}
</div>
</div>
</TableCell>
@@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Eye, Play, Pause, Square, Trash2, BarChart3, RefreshCw } from "lucide-react";
import { DockerContainer } from "@/types/docker.types";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerRowActionsProps {
container: DockerContainer;
@@ -13,12 +12,11 @@ interface DockerRowActionsProps {
}
export const DockerRowActions = ({ container, containerStatus, onContainerAction, onViewMetrics }: DockerRowActionsProps) => {
const { t } = useLanguage();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-muted">
<span className="sr-only">{t('openMenu', 'docker')}</span>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -28,14 +26,14 @@ export const DockerRowActions = ({ container, containerStatus, onContainerAction
className="cursor-pointer hover:bg-muted"
>
<BarChart3 className="mr-2 h-4 w-4" />
{t('viewMetrics', 'docker')}
View Metrics
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onContainerAction('view-detail', container.id, container.name)}
className="cursor-pointer hover:bg-muted"
>
<Eye className="mr-2 h-4 w-4" />
{t('viewDetails', 'docker')}
View Details
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -1,20 +1,18 @@
import { TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useLanguage } from "@/contexts/LanguageContext";
export const DockerTableHeader = () => {
const { t } = useLanguage();
return (
<TableHeader>
<TableRow className="border-border bg-muted/30">
<TableHead className="min-w-[200px] font-semibold">{t('container', 'docker')}</TableHead>
<TableHead className="min-w-[100px] font-semibold">{t('status', 'docker')}</TableHead>
<TableHead className="min-w-[140px] font-semibold">{t('cpuUsage', 'docker')}</TableHead>
<TableHead className="min-w-[160px] font-semibold">{t('memory', 'docker')}</TableHead>
<TableHead className="min-w-[160px] font-semibold">{t('disk', 'docker')}</TableHead>
<TableHead className="min-w-[100px] font-semibold">{t('uptime', 'docker')}</TableHead>
<TableHead className="min-w-[160px] font-semibold">{t('lastChecked', 'docker')}</TableHead>
<TableHead className="min-w-[80px] text-center font-semibold">{t('actions', 'docker')}</TableHead>
<TableHead className="min-w-[200px] font-semibold">Container</TableHead>
<TableHead className="min-w-[100px] font-semibold">Status</TableHead>
<TableHead className="min-w-[140px] font-semibold">CPU Usage</TableHead>
<TableHead className="min-w-[160px] font-semibold">Memory</TableHead>
<TableHead className="min-w-[160px] font-semibold">Disk</TableHead>
<TableHead className="min-w-[100px] font-semibold">Uptime</TableHead>
<TableHead className="min-w-[160px] font-semibold">Last Checked</TableHead>
<TableHead className="min-w-[80px] text-center font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
);
@@ -2,7 +2,6 @@
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search, RefreshCw } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerTableSearchProps {
searchTerm: string;
@@ -12,13 +11,12 @@ interface DockerTableSearchProps {
}
export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoading }: DockerTableSearchProps) => {
const { t } = useLanguage();
return (
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder={t('searchContainersPlaceholder', 'docker')}
placeholder="Search containers..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 sm:w-64 bg-background border-border"
@@ -32,7 +30,7 @@ export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoa
className="w-full sm:w-auto bg-background border-border hover:bg-muted"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
<span className="sm:inline">{t('refresh', 'docker')}</span>
<span className="sm:inline">Refresh</span>
</Button>
</div>
);
@@ -1,4 +1,3 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -7,11 +6,10 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Plus, X, Server } from 'lucide-react';
import { Plus, X, Server, Shield, AlertTriangle } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
import { useLanguage } from "@/contexts/LanguageContext";
interface ComponentsSelectorProps {
selectedComponents: Partial<StatusPageComponentRecord>[];
@@ -19,8 +17,13 @@ interface ComponentsSelectorProps {
onComponentDelete?: (componentId: string) => void;
}
const componentTypes = [
{ value: 'uptime', label: 'Uptime Service', icon: Server },
{ value: 'ssl', label: 'SSL Certificate', icon: Shield },
{ value: 'incident', label: 'Incident Monitoring', icon: AlertTriangle },
];
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
const { t } = useLanguage();
const [showAddForm, setShowAddForm] = useState(false);
const [newComponent, setNewComponent] = useState({
name: '',
@@ -73,16 +76,16 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
{t('statusPageComponents')}
Status Page Components
</CardTitle>
<CardDescription>
{t('addMonitoringComponentsDesc')}
Add monitoring components like uptime services, SSL certificates, and incident tracking
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedComponents.length > 0 && (
<div className="space-y-2">
<Label>{t('selectedComponents')}</Label>
<Label>Selected Components</Label>
<div className="space-y-2">
{selectedComponents.map((component, index) => (
<div key={component.id || index} className="flex items-center justify-between p-3 border rounded-lg">
@@ -94,12 +97,12 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<div className="flex gap-2 mt-1">
{component.service_id && (
<Badge variant="secondary" className="text-xs">
{t('service')}: {services.find(s => s.id === component.service_id)?.name || component.service_id}
Service: {services.find(s => s.id === component.service_id)?.name || component.service_id}
</Badge>
)}
{component.server_id && (
<Badge variant="secondary" className="text-xs">
{t('server')}: {component.server_id}
Server: {component.server_id}
</Badge>
)}
</div>
@@ -124,22 +127,22 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
{t('addComponent')}
Add Component
</Button>
) : (
<div className="border rounded-lg p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="component-name">{t('componentName')}</Label>
<Label htmlFor="component-name">Component Name</Label>
<Input
id="component-name"
placeholder={t('componentNamePlaceholder')}
placeholder="e.g., Main Website"
value={newComponent.name}
onChange={(e) => setNewComponent({ ...newComponent, name: e.target.value })}
/>
</div>
<div>
<Label htmlFor="display-order">{t('displayOrder')}</Label>
<Label htmlFor="display-order">Display Order</Label>
<Input
id="display-order"
type="number"
@@ -150,10 +153,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</div>
<div>
<Label htmlFor="component-description">{t('descriptionOptional')}</Label>
<Label htmlFor="component-description">Description (Optional)</Label>
<Textarea
id="component-description"
placeholder={t('descriptionPlaceholder')}
placeholder="Brief description of this component"
value={newComponent.description}
onChange={(e) => setNewComponent({ ...newComponent, description: e.target.value })}
/>
@@ -161,12 +164,12 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="service-id">{t('uptimeServiceOptional')}</Label>
<Label htmlFor="service-id">Uptime Service (Optional)</Label>
<Select onValueChange={(value) => setNewComponent({ ...newComponent, service_id: value })}>
<SelectTrigger>
<SelectValue placeholder={t('selectUptimeService')} />
<SelectValue placeholder="Select an uptime service" />
</SelectTrigger>
<SelectContent>
<SelectContent className="z-50 bg-white border shadow-lg">
{services.map((service) => (
<SelectItem key={service.id} value={service.id}>
<div className="flex items-center gap-2">
@@ -183,10 +186,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</Select>
</div>
<div>
<Label htmlFor="server-id">{t('serverIdOptional')}</Label>
<Label htmlFor="server-id">Server ID (Optional)</Label>
<Input
id="server-id"
placeholder={t('serverIdPlaceholder')}
placeholder="server_456"
value={newComponent.server_id}
onChange={(e) => setNewComponent({ ...newComponent, server_id: e.target.value })}
/>
@@ -195,14 +198,45 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<div className="flex gap-2">
<Button onClick={addComponent} disabled={!newComponent.name.trim()}>
{t('addComponent')}
Add Component
</Button>
<Button variant="outline" onClick={() => setShowAddForm(false)}>
{t('cancel')}
Cancel
</Button>
</div>
</div>
)}
<div className="mt-4">
<Label>Quick Add Templates</Label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-2">
{componentTypes.map((type) => {
const Icon = type.icon;
return (
<Button
key={type.value}
variant="outline"
size="sm"
onClick={() => {
const component: Partial<StatusPageComponentRecord> = {
name: type.label,
description: `Monitor ${type.label.toLowerCase()}`,
service_id: '',
server_id: '',
display_order: selectedComponents.length + 1,
operational_status_id: '',
};
onComponentsChange([...selectedComponents, component]);
}}
className="justify-start"
>
<Icon className="h-4 w-4 mr-2" />
{type.label}
</Button>
);
})}
</div>
</div>
</CardContent>
</Card>
);
@@ -14,7 +14,6 @@ import { useCreateStatusPageComponent } from '@/hooks/useStatusPageComponents';
import { ComponentsSelector } from './ComponentsSelector';
import { Plus } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({
title: z.string().min(1, 'Title is required'),
@@ -32,7 +31,6 @@ const formSchema = z.object({
type FormData = z.infer<typeof formSchema>;
export const CreateOperationalPageDialog = () => {
const { t } = useLanguage();
const [open, setOpen] = useState(false);
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
const createMutation = useCreateOperationalPage();
@@ -69,13 +67,13 @@ export const CreateOperationalPageDialog = () => {
page_style: data.page_style || '',
};
// console.log('Creating operational page with payload:', payload);
console.log('Creating operational page with payload:', payload);
const createdPage = await createMutation.mutateAsync(payload);
// console.log('Created page:', createdPage);
console.log('Created page:', createdPage);
// Create components after page is created
if (selectedComponents.length > 0) {
// console.log('Creating components for page:', createdPage.id);
console.log('Creating components for page:', createdPage.id);
for (const component of selectedComponents) {
const componentPayload = {
operational_status_id: createdPage.id,
@@ -86,7 +84,7 @@ export const CreateOperationalPageDialog = () => {
display_order: component.display_order || 1,
};
// console.log('Creating component with payload:', componentPayload);
console.log('Creating component with payload:', componentPayload);
await createComponentMutation.mutateAsync(componentPayload);
}
}
@@ -95,7 +93,7 @@ export const CreateOperationalPageDialog = () => {
form.reset();
setSelectedComponents([]);
} catch (error) {
// console.error('Error creating operational page:', error);
console.error('Error creating operational page:', error);
}
};
@@ -104,14 +102,14 @@ export const CreateOperationalPageDialog = () => {
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
{t('createPage')}
Create Page
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('createOperationalPage')}</DialogTitle>
<DialogTitle>Create Operational Page</DialogTitle>
<DialogDescription>
{t('createOperationalPageDesc')}
Create a new operational status page to monitor your services and components.
</DialogDescription>
</DialogHeader>
@@ -123,9 +121,9 @@ export const CreateOperationalPageDialog = () => {
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>{t('title')}</FormLabel>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder={t('myServiceStatusPlaceholder')} {...field} />
<Input placeholder="My Service Status" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -137,9 +135,9 @@ export const CreateOperationalPageDialog = () => {
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>{t('slug')}</FormLabel>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input placeholder={t('myServiceStatusSlugPlaceholder')} {...field} />
<Input placeholder="my-service-status" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -152,10 +150,10 @@ export const CreateOperationalPageDialog = () => {
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('description')}</FormLabel>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder={t('operationalPageDescriptionPlaceholder')}
placeholder="A brief description of your operational page"
className="min-h-[80px]"
{...field}
/>
@@ -171,17 +169,17 @@ export const CreateOperationalPageDialog = () => {
name="theme"
render={({ field }) => (
<FormItem>
<FormLabel>{t('theme')}</FormLabel>
<FormLabel>Theme</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectTheme')} />
<SelectValue placeholder="Select theme" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="default">{t('themeDefault')}</SelectItem>
<SelectItem value="dark">{t('themeDark')}</SelectItem>
<SelectItem value="light">{t('themeLight')}</SelectItem>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="light">Light</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -194,18 +192,18 @@ export const CreateOperationalPageDialog = () => {
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>{t('initialStatus')}</FormLabel>
<FormLabel>Initial Status</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectStatus')} />
<SelectValue placeholder="Select status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="operational">{t('statusOperational')}</SelectItem>
<SelectItem value="degraded">{t('statusDegraded')}</SelectItem>
<SelectItem value="maintenance">{t('statusMaintenance')}</SelectItem>
<SelectItem value="major_outage">{t('statusMajorOutage')}</SelectItem>
<SelectItem value="operational">Operational</SelectItem>
<SelectItem value="degraded">Degraded Performance</SelectItem>
<SelectItem value="maintenance">Under Maintenance</SelectItem>
<SelectItem value="major_outage">Major Outage</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -220,9 +218,9 @@ export const CreateOperationalPageDialog = () => {
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>{t('publicPage')}</FormLabel>
<FormLabel>Public Page</FormLabel>
<FormDescription>
{t('makePagePublic')}
Make this page publicly accessible
</FormDescription>
</div>
<FormControl>
@@ -240,12 +238,12 @@ export const CreateOperationalPageDialog = () => {
name="custom_domain"
render={({ field }) => (
<FormItem>
<FormLabel>{t('customDomainOptional')}</FormLabel>
<FormLabel>Custom Domain (Optional)</FormLabel>
<FormControl>
<Input placeholder={t('customDomainPlaceholder')} {...field} />
<Input placeholder="status.yourdomain.com" {...field} />
</FormControl>
<FormDescription>
{t('customDomainDescription')}
Custom domain for your status page
</FormDescription>
<FormMessage />
</FormItem>
@@ -263,13 +261,13 @@ export const CreateOperationalPageDialog = () => {
variant="outline"
onClick={() => setOpen(false)}
>
{t('cancel')}
Cancel
</Button>
<Button
type="submit"
disabled={createMutation.isPending || createComponentMutation.isPending}
>
{createMutation.isPending || createComponentMutation.isPending ? t('creating') : t('createPage')}
{createMutation.isPending || createComponentMutation.isPending ? 'Creating...' : 'Create Page'}
</Button>
</div>
</form>
@@ -14,7 +14,6 @@ import { useCreateStatusPageComponent, useStatusPageComponentsByOperationalId, u
import { ComponentsSelector } from './ComponentsSelector';
import { OperationalPageRecord } from '@/types/operational.types';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({
title: z.string().min(1, 'Title is required'),
@@ -38,7 +37,6 @@ interface EditOperationalPageDialogProps {
}
export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOperationalPageDialogProps) => {
const { t } = useLanguage();
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
const [componentsLoaded, setComponentsLoaded] = useState(false);
@@ -197,9 +195,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('editOperationalPage')}</DialogTitle>
<DialogTitle>Edit Operational Page</DialogTitle>
<DialogDescription>
{t('updateYourOperationalPage')}
Update your operational status page settings and manage components.
</DialogDescription>
</DialogHeader>
@@ -211,9 +209,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>{t('title')}</FormLabel>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder={t('myServiceStatusPlaceholder')} {...field} />
<Input placeholder="My Service Status" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -225,9 +223,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>{t('slug')}</FormLabel>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input placeholder={t('myServiceStatusSlugPlaceholder')} {...field} />
<Input placeholder="my-service-status" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -240,10 +238,10 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('description')}</FormLabel>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder={t('operationalPageDescriptionPlaceholder')}
placeholder="A brief description of your operational page"
className="min-h-[80px]"
{...field}
/>
@@ -259,17 +257,17 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="theme"
render={({ field }) => (
<FormItem>
<FormLabel>{t('theme')}</FormLabel>
<FormLabel>Theme</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectTheme')} />
<SelectValue placeholder="Select theme" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="default">{t('themeDefault')}</SelectItem>
<SelectItem value="dark">{t('themeDark')}</SelectItem>
<SelectItem value="light">{t('themeLight')}</SelectItem>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="light">Light</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -282,18 +280,18 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>{t('status')}</FormLabel>
<FormLabel>Status</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectStatus')} />
<SelectValue placeholder="Select status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="operational">{t('statusOperational')}</SelectItem>
<SelectItem value="degraded">{t('statusDegraded')}</SelectItem>
<SelectItem value="maintenance">{t('statusMaintenance')}</SelectItem>
<SelectItem value="major_outage">{t('statusMajorOutage')}</SelectItem>
<SelectItem value="operational">Operational</SelectItem>
<SelectItem value="degraded">Degraded Performance</SelectItem>
<SelectItem value="maintenance">Under Maintenance</SelectItem>
<SelectItem value="major_outage">Major Outage</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -308,9 +306,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>{t('publicPage')}</FormLabel>
<FormLabel>Public Page</FormLabel>
<FormDescription>
{t('makePagePublic')}
Make this page publicly accessible
</FormDescription>
</div>
<FormControl>
@@ -328,12 +326,12 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="custom_domain"
render={({ field }) => (
<FormItem>
<FormLabel>{t('customDomainOptional')}</FormLabel>
<FormLabel>Custom Domain (Optional)</FormLabel>
<FormControl>
<Input placeholder={t('customDomainPlaceholder')} {...field} />
<Input placeholder="status.yourdomain.com" {...field} />
</FormControl>
<FormDescription>
{t('customDomainDescription')}
Custom domain for your status page
</FormDescription>
<FormMessage />
</FormItem>
@@ -352,13 +350,13 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
variant="outline"
onClick={() => onOpenChange(false)}
>
{t('cancel')}
Cancel
</Button>
<Button
type="submit"
disabled={isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending}
>
{isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending ? t('updating') : t('updatePage')}
{isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending ? 'Updating...' : 'Update Page'}
</Button>
</div>
</form>
@@ -6,7 +6,6 @@ import { OperationalPageRecord } from '@/types/operational.types';
import { StatusBadge } from './StatusBadge';
import { Globe, ExternalLink, Eye, Settings, Trash2 } from 'lucide-react';
import { format } from 'date-fns';
import { useLanguage } from "@/contexts/LanguageContext";
interface OperationalPageCardProps {
page: OperationalPageRecord;
@@ -16,8 +15,6 @@ interface OperationalPageCardProps {
}
export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: OperationalPageCardProps) => {
const { t } = useLanguage();
return (
<Card className="hover:shadow-lg transition-shadow duration-200">
<CardHeader className="pb-3">
@@ -35,23 +32,23 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-muted-foreground">{t('slug')}:</span>
<span className="font-medium text-muted-foreground">Slug:</span>
<p className="mt-1">{page.slug}</p>
</div>
<div>
<span className="font-medium text-muted-foreground">{t('theme')}:</span>
<span className="font-medium text-muted-foreground">Theme:</span>
<p className="mt-1 capitalize">{page.theme}</p>
</div>
<div>
<span className="font-medium text-muted-foreground">{t('public')}:</span>
<div className="mt-1">
<span className="font-medium text-muted-foreground">Public:</span>
<p className="mt-1">
<Badge variant={page.is_public === 'true' ? 'default' : 'secondary'}>
{page.is_public === 'true' ? t('yes') : t('no')}
{page.is_public === 'true' ? 'Yes' : 'No'}
</Badge>
</div>
</p>
</div>
<div>
<span className="font-medium text-muted-foreground">{t('updated')}:</span>
<span className="font-medium text-muted-foreground">Updated:</span>
<p className="mt-1">{format(new Date(page.updated), 'MMM dd, yyyy')}</p>
</div>
</div>
@@ -73,7 +70,7 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
className="flex-1"
>
<Eye className="h-4 w-4 mr-2" />
{t('view')}
View
</Button>
)}
{onEdit && (
@@ -84,7 +81,7 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
className="flex-1"
>
<Settings className="h-4 w-4 mr-2" />
{t('edit')}
Edit
</Button>
)}
{onDelete && (
@@ -9,8 +9,6 @@ import { Button } from '@/components/ui/button';
import { OperationalPageRecord } from '@/types/operational.types';
import { Activity, Plus, RefreshCw } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { useLanguage } from "@/contexts/LanguageContext";
import {
AlertDialog,
AlertDialogAction,
@@ -23,7 +21,6 @@ import {
} from '@/components/ui/alert-dialog';
export const OperationalPageContent = () => {
const { t } = useLanguage();
const navigate = useNavigate();
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
const deleteMutation = useDeleteOperationalPage();
@@ -71,13 +68,13 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">{t('failedToLoadOperationalPages')}</h3>
<h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
<p className="text-muted-foreground mb-4">
{t('loadingoperationalPages')}
There was an error loading your operational pages. Please try again.
</p>
<Button onClick={() => refetch()} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
{t('tryagain')}
Try Again
</Button>
</div>
</div>
@@ -89,9 +86,9 @@ export const OperationalPageContent = () => {
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2"> {t('operationalPages')}</h1>
<h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
<p className="text-muted-foreground">
{t('describeOperation')}
Manage your public status pages and monitor service health
</p>
</div>
@@ -103,7 +100,7 @@ export const OperationalPageContent = () => {
disabled={isRefetching}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
{t('refresh')}
Refresh
</Button>
<CreateOperationalPageDialog />
</div>
@@ -138,9 +135,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">{t('noOperationalPagesFound')}</h3>
<h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
<p className="text-muted-foreground mb-6">
{t('createYourFirstOperationalPage')}
Create your first operational page to start monitoring your services and communicate status to your users.
</p>
<CreateOperationalPageDialog />
</CardContent>
@@ -167,14 +164,14 @@ export const OperationalPageContent = () => {
<div className="mt-8 pt-6 border-t">
<div className="flex flex-wrap gap-6 text-sm text-muted-foreground">
<div>
<span className="font-medium">{t('totalPages')}:</span> {pages.length}
<span className="font-medium">Total Pages:</span> {pages.length}
</div>
<div>
<span className="font-medium">{t('totalPages')}:</span>{' '}
<span className="font-medium">Public Pages:</span>{' '}
{pages.filter(p => p.is_public === 'true').length}
</div>
<div>
<span className="font-medium">{t('operational')}:</span>{' '}
<span className="font-medium">Operational:</span>{' '}
{pages.filter(p => p.status === 'operational').length}
</div>
</div>
@@ -192,19 +189,19 @@ export const OperationalPageContent = () => {
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteOperationalPage')}</AlertDialogTitle>
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteOperationalPageConfirm').replace('{title}', pageToDelete?.title ?? '')}
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>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? t('deleting') : t('delete')}
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -5,7 +5,6 @@ import { format } from 'date-fns';
import { OperationalPageRecord } from '@/types/operational.types';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { Service } from '@/types/service.types';
import { useLanguage } from '@/contexts/LanguageContext';
interface CurrentStatusSectionProps {
page: OperationalPageRecord;
@@ -46,18 +45,18 @@ const getActualStatus = (components: StatusPageComponentRecord[], services: Serv
return 'operational';
};
const getStatusMessage = (status: OperationalPageRecord['status'], t: (k: string, m?: string) => string) => {
const getStatusMessage = (status: OperationalPageRecord['status']) => {
switch (status) {
case 'operational':
return t('allOperational', 'public');
return 'All systems are operational';
case 'degraded':
return t('degradedPerformance', 'public');
return 'Some systems are experiencing degraded performance';
case 'maintenance':
return t('underMaintenance', 'public');
return 'Systems are currently under maintenance';
case 'major_outage':
return t('majorOutage', 'public');
return 'We are experiencing a major service outage';
default:
return t('statusUnknown', 'public');
return 'Status unknown';
}
};
@@ -107,7 +106,6 @@ const getStatusBackground = (status: OperationalPageRecord['status']) => {
};
export const CurrentStatusSection = ({ page, components, services }: CurrentStatusSectionProps) => {
const { t } = useLanguage();
const actualStatus = getActualStatus(components, services);
const displayStatus = actualStatus; // Use actual status for real-time accuracy
@@ -116,7 +114,7 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
<CardHeader>
<CardTitle className="flex items-center gap-3 text-card-foreground text-xl">
<Shield className="h-6 w-6" />
{t('systemStatus', 'public')}
System Status
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
@@ -125,10 +123,10 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
{getStatusIcon(displayStatus)}
<div>
<h3 className={`text-2xl font-bold ${getStatusColor(displayStatus)}`}>
{getStatusMessage(displayStatus, t)}
{getStatusMessage(displayStatus)}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t('autoUpdatedByHealth', 'public')}
Status automatically updated based on component health
</p>
</div>
</div>
@@ -138,20 +136,20 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
displayStatus === 'maintenance' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
}`}>
{displayStatus === 'operational' ? t('allOperational', 'public') :
displayStatus === 'degraded' ? t('degradedPerformance', 'public') :
displayStatus === 'maintenance' ? t('underMaintenance', 'public') : t('majorOutage', 'public')}
{displayStatus === 'operational' ? 'All Systems Operational' :
displayStatus === 'degraded' ? 'Degraded Performance' :
displayStatus === 'maintenance' ? 'Under Maintenance' : 'Major Outage'}
</div>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground border-t pt-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>{t('lastUpdatedAt', 'public', { time: format(new Date(), 'MMM dd, yyyy HH:mm') })}</span>
<span>Last updated: {format(new Date(), 'MMM dd, yyyy HH:mm')} UTC</span>
</div>
<div className="flex items-center gap-2">
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
<span>{t('liveStatusMonitoring', 'public')}</span>
<span>Live status monitoring</span>
</div>
</div>
</CardContent>
@@ -9,12 +9,10 @@ import { CurrentStatusSection } from './CurrentStatusSection';
import { ComponentsStatusSection } from './ComponentsStatusSection';
import { OverallUptimeSection } from './OverallUptimeSection';
import { PublicStatusPageFooter } from './PublicStatusPageFooter';
import { useLanguage } from '@/contexts/LanguageContext';
export const PublicStatusPage = () => {
const { t } = useLanguage();
const { slug } = useParams<{ slug: string }>();
// console.log('PublicStatusPage - slug from params:', slug);
console.log('PublicStatusPage - slug from params:', slug);
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug);
const [lastUpdated, setLastUpdated] = useState(new Date());
@@ -53,7 +51,7 @@ export const PublicStatusPage = () => {
};
}, [page?.theme]);
// console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
if (loading) {
return (
@@ -61,9 +59,9 @@ export const PublicStatusPage = () => {
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<div className="space-y-2">
<p className="text-lg font-medium text-foreground">{t('loadingStatusPage', 'public')}</p>
<p className="text-sm text-muted-foreground">{t('fetchingRealtimeStatus', 'public')}</p>
<p className="text-xs text-muted-foreground">{t('slugLabel', 'public')}: {slug || 'No slug provided'}</p>
<p className="text-lg font-medium text-foreground">Loading Status Page</p>
<p className="text-sm text-muted-foreground">Fetching real-time system status...</p>
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
</div>
</div>
</div>
@@ -78,19 +76,19 @@ export const PublicStatusPage = () => {
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
</div>
<div className="space-y-2">
<h1 className="text-2xl font-bold text-foreground">{t('statusPageNotFound', 'public')}</h1>
<h1 className="text-2xl font-bold text-foreground">Status Page Not Found</h1>
<p className="text-muted-foreground">
{error || t('notFoundDescription', 'public')}
{error || 'The requested status page could not be found or is not publicly accessible.'}
</p>
<p className="text-xs text-muted-foreground">{t('slugLabel', 'public')}: {slug || 'No slug provided'}</p>
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
</div>
<div className="flex gap-3 justify-center">
<Button onClick={() => window.history.back()} variant="outline">
{t('goBack', 'public')}
Go Back
</Button>
<Button onClick={() => window.location.reload()} className="gap-2">
<RefreshCw className="h-4 w-4" />
{t('retry', 'public')}
Retry
</Button>
</div>
</div>
@@ -19,61 +19,61 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
useEffect(() => {
const fetchPublicPage = async () => {
if (!slug) {
// console.log('No slug provided');
console.log('No slug provided');
setError('No status page slug provided');
setLoading(false);
return;
}
try {
// console.log('Fetching public status page for slug:', slug);
console.log('Fetching public status page for slug:', slug);
setLoading(true);
setError(null);
// Fetch operational page
// console.log('Fetching operational pages...');
console.log('Fetching operational pages...');
const pages = await operationalPageService.getOperationalPages();
// console.log('All pages:', pages);
console.log('All pages:', pages);
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
// console.log('Found page:', foundPage);
console.log('Found page:', foundPage);
if (!foundPage) {
// console.log('Page not found or not public');
console.log('Page not found or not public');
setError('Status page not found or not public');
setLoading(false);
return;
}
setPage(foundPage);
// console.log('Page set successfully');
console.log('Page set successfully');
// Fetch components for this page
// console.log('Fetching components for page:', foundPage.id);
console.log('Fetching components for page:', foundPage.id);
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
// console.log('Components found:', pageComponents);
console.log('Components found:', pageComponents);
setComponents(pageComponents);
// Fetch all services
// console.log('Fetching all services...');
console.log('Fetching all services...');
const allServices = await serviceService.getServices();
// console.log('Services found:', allServices);
console.log('Services found:', allServices);
setServices(allServices);
// Fetch uptime data for each component that has a service
// console.log('Fetching uptime data...');
console.log('Fetching uptime data...');
const uptimePromises = pageComponents
.filter(component => component.service_id)
.map(async (component) => {
try {
// console.log('Fetching uptime for service:', component.service_id);
console.log('Fetching uptime for service:', component.service_id);
const endDate = new Date();
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
const history = await uptimeService.getUptimeHistory(component.service_id, 2000, startDate, endDate);
// console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
return { serviceId: component.service_id, history };
} catch (error) {
// console.error(`Error fetching uptime for service ${component.service_id}:`, error);
console.error(`Error fetching uptime for service ${component.service_id}:`, error);
return { serviceId: component.service_id, history: [] };
}
});
@@ -84,12 +84,12 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
uptimeMap[result.serviceId] = result.history;
});
setUptimeData(uptimeMap);
// console.log('Uptime data set successfully');
console.log('Uptime data set successfully');
// console.log('All data fetched successfully');
console.log('All data fetched successfully');
} catch (err) {
// console.error('Error fetching public page:', err);
console.error('Error fetching public page:', err);
setError(`Failed to load status page: ${err}`);
} finally {
setLoading(false);
@@ -1,12 +1,15 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Copy, Download, Terminal, CheckCircle, Zap, Play } from "lucide-react";
import { regionalService } from "@/services/regionalService";
import { InstallCommand } from "@/types/regional.types";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
import { getCurrentEndpoint } from "@/lib/pocketbase";
import { RegionalAgentConfigForm } from "./RegionalAgentConfigForm";
import { RegionalOneClickTab } from "./RegionalOneClickTab";
import { RegionalManualTab } from "./RegionalManualTab";
interface AddRegionalAgentDialogProps {
open: boolean;
@@ -19,54 +22,30 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
onOpenChange,
onAgentAdded
}) => {
const { t } = useLanguage();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("configure");
const [formData, setFormData] = useState({
regionName: "",
agentIp: "",
});
const [agentToken, setAgentToken] = useState("");
const [agentId, setAgentId] = useState("");
const [currentPocketBaseUrl, setCurrentPocketBaseUrl] = useState("");
const [step, setStep] = useState(1);
const [regionName, setRegionName] = useState("");
const [agentIp, setAgentIp] = useState("");
const [installCommand, setInstallCommand] = useState<InstallCommand | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
// Generate new credentials when dialog opens or after successful creation
const generateNewCredentials = () => {
const newToken = `rgn_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
const newAgentId = `regional_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
setAgentToken(newToken);
setAgentId(newAgentId);
};
useEffect(() => {
if (open) {
const endpoint = getCurrentEndpoint();
setCurrentPocketBaseUrl(endpoint);
generateNewCredentials();
}
}, [open]);
const handleSubmit = async (e: React.FormEvent) => {
const handleCreateAgent = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.regionName.trim() || !formData.agentIp.trim()) return;
if (!regionName.trim() || !agentIp.trim()) return;
setIsSubmitting(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
toast({
title: t('success'),
description: t('agentCreatedSuccessfully'),
const result = await regionalService.createRegionalService({
region_name: regionName,
agent_ip_address: agentIp,
});
setActiveTab("one-click");
generateNewCredentials();
onAgentAdded();
setInstallCommand(result.installCommand);
setStep(2);
} catch (error) {
toast({
title: t('error'),
description: t('failedToCreateAgent'),
title: "Error",
description: "Failed to create regional agent configuration.",
variant: "destructive",
});
} finally {
@@ -74,65 +53,407 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
}
};
const handleDialogClose = () => {
setFormData({
regionName: "",
agentIp: "",
const copyToClipboard = async (text: string, description: string = "Content") => {
try {
// Try the modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
toast({
title: "Copied!",
description: `${description} copied to clipboard.`,
});
return;
}
// Fallback for older browsers or non-secure contexts (like localhost)
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
toast({
title: "Copied!",
description: `${description} copied to clipboard.`,
});
} else {
throw new Error('execCommand failed');
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// Show the text in a modal or alert as final fallback
const userAgent = navigator.userAgent.toLowerCase();
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
let errorMessage = "Failed to copy to clipboard.";
if (isLocalhost) {
errorMessage += " This is common in local development. Please manually copy the text.";
} else if (userAgent.includes('chrome')) {
errorMessage += " Try using HTTPS or enable clipboard permissions.";
}
toast({
title: "Copy failed",
description: errorMessage,
variant: "destructive",
});
// As a last resort, select the text for manual copying
try {
const textarea = document.querySelector('textarea[readonly]') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
}
} catch (selectError) {
console.error('Failed to select text:', selectError);
}
}
};
const downloadScript = () => {
if (!installCommand) return;
const blob = new Blob([installCommand.bash_script], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `install-regional-agent-${installCommand.agent_id}.sh`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: "Downloaded!",
description: "Installation script downloaded successfully.",
});
setActiveTab("configure");
generateNewCredentials();
};
const handleComplete = () => {
onAgentAdded();
setStep(1);
setRegionName("");
setAgentIp("");
setInstallCommand(null);
onOpenChange(false);
};
const resetDialog = () => {
setStep(1);
setRegionName("");
setAgentIp("");
setInstallCommand(null);
};
return (
<Dialog open={open} onOpenChange={handleDialogClose}>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
<DialogHeader>
<DialogTitle>{t('addRegionalMonitoringAgent')}</DialogTitle>
<DialogTitle>Add Regional Monitoring Agent</DialogTitle>
<DialogDescription>
{t('deployRegionalMonitoringAgent')}
Deploy a regional monitoring agent with automatic one-click installation.
</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="configure">{t('configureAgent')}</TabsTrigger>
<TabsTrigger value="one-click">{t('oneClickInstallTab')}</TabsTrigger>
<TabsTrigger value="manual">{t('manualInstallTab')}</TabsTrigger>
</TabsList>
{step === 1 && (
<form onSubmit={handleCreateAgent} className="space-y-6">
<div className="space-y-4">
<div>
<Label htmlFor="regionName">Region Name</Label>
<Input
id="regionName"
placeholder="e.g., us-east-1, europe-west-1, asia-pacific-1"
value={regionName}
onChange={(e) => setRegionName(e.target.value)}
required
/>
</div>
<TabsContent value="configure" className="space-y-6">
<RegionalAgentConfigForm
formData={formData}
setFormData={setFormData}
agentId={agentId}
agentToken={agentToken}
currentPocketBaseUrl={currentPocketBaseUrl}
isSubmitting={isSubmitting}
onSubmit={handleSubmit}
/>
</TabsContent>
<div>
<Label htmlFor="agentIp">Agent Server IP Address</Label>
<Input
id="agentIp"
placeholder="e.g., 192.168.1.100 or your-server.example.com"
value={agentIp}
onChange={(e) => setAgentIp(e.target.value)}
required
/>
</div>
</div>
<TabsContent value="one-click" className="space-y-6">
<RegionalOneClickTab
agentToken={agentToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
agentId={agentId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Generating..." : "Generate Installation"}
</Button>
</div>
</form>
)}
<TabsContent value="manual" className="space-y-6">
<RegionalManualTab
agentToken={agentToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
agentId={agentId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
</Tabs>
{step === 2 && installCommand && (
<div className="space-y-6">
<div className="text-center space-y-2">
<CheckCircle className="h-12 w-12 text-green-600 mx-auto" />
<h3 className="text-lg font-semibold">Agent Configuration Ready!</h3>
<p className="text-muted-foreground">
One-click installation script generated with automatic configuration.
</p>
</div>
<Tabs defaultValue="oneclicK" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="oneclicK">One-Click Install</TabsTrigger>
<TabsTrigger value="details">Agent Details</TabsTrigger>
<TabsTrigger value="manual">Manual Install</TabsTrigger>
</TabsList>
<TabsContent value="oneclicK" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
One-Click Automatic Installation
</CardTitle>
<CardDescription>
Complete installation, configuration, and service startup in one command
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-green-800 font-medium mb-2">
<Play className="h-4 w-4" />
What this script does automatically:
</div>
<ul className="text-sm text-green-700 space-y-1 ml-6">
<li> Downloads the latest .deb package</li>
<li> Installs the regional monitoring agent</li>
<li> Creates configuration file with your settings</li>
<li> Starts and enables the service</li>
<li> Runs health checks</li>
</ul>
</div>
<div className="space-y-2">
<Label>Run this command on your target server:</Label>
<div className="relative">
<Textarea
readOnly
value={`# One-click installation command:
curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
--region-name="${regionName}" \\
--agent-id="${installCommand.agent_id}" \\
--agent-ip="${agentIp}" \\
--token="${installCommand.token}" \\
--pocketbase-url="${installCommand.api_endpoint}"`}
className="font-mono text-sm min-h-[120px] pr-12"
/>
<Button
size="sm"
variant="outline"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div className="flex gap-2">
<Button onClick={downloadScript} variant="outline" className="flex-1">
<Download className="mr-2 h-4 w-4" />
Download Complete Script
</Button>
<Button
onClick={() => copyToClipboard(installCommand.bash_script, "Installation script")}
variant="outline"
className="flex-1"
>
<Copy className="mr-2 h-4 w-4" />
Copy Full Script
</Button>
</div>
{/* Local development notice */}
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
<strong>Local Development Notice:</strong> If clipboard copying fails, this is normal in local development.
You can use the "Download Complete Script" button instead or manually copy the text.
</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="details" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Generated Agent Configuration</CardTitle>
<CardDescription>
These values will be automatically configured during installation
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-foreground">Agent ID</Label>
<div className="relative">
<Input
value={installCommand.agent_id}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
/>
<Button
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.agent_id, "Agent ID")}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Region Name</Label>
<Input
value={regionName}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Server IP</Label>
<Input
value={agentIp}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground">API Endpoint</Label>
<Input
value={installCommand.api_endpoint}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
/>
</div>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Authentication Token</Label>
<div className="relative">
<Input
value={installCommand.token}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
/>
<Button
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.token, "Authentication token")}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Configuration file location:</strong> /etc/regional-check-agent/regional-check-agent.env
</p>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="manual" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
Manual Installation Steps
</CardTitle>
<CardDescription>
Step-by-step manual installation process
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-sm space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 1: Download Package</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
<p className="text-xs text-muted-foreground mt-1"> If youre using the amd64 architecture, please download this package. </p> <br/>
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb
</code>
<code className="text-xs bg-muted p-2 rounded block mt-1">
<p className="text-xs text-muted-foreground mt-1"> If youre using the arm64 architecture, please download this package. </p> <br/>
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb
</code>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 2: Install Package</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb<br/>
sudo apt-get install -f
</code>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 3: Configure Agent</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo nano /etc/regional-check-agent/regional-check-agent.env
</code>
<code className="text-xs bg-muted p-2 rounded block mt-1">
Copy sample configuration from https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf
</code>
<p className="text-xs text-muted-foreground mt-1">Add the configuration values shown in the Agent Details tab</p>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 4: Start Service</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo systemctl enable regional-check-agent<br/>
sudo systemctl start regional-check-agent
</code>
</div>
<div className="border-l-4 border-green-500 pl-4">
<p className="font-medium">Step 5: Verify Installation</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo systemctl status regional-check-agent<br/>
curl http://localhost:8091/health
</code>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={resetDialog}>
Add Another Agent
</Button>
<Button onClick={handleComplete}>
Complete Setup
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
);
@@ -8,7 +8,6 @@ import { MapPin, Wifi, WifiOff, MoreVertical, Trash2, Terminal, Copy } from "luc
import { RegionalService } from "@/types/regional.types";
import { formatDistanceToNow } from "date-fns";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
interface RegionalAgentCardProps {
agent: RegionalService;
@@ -17,7 +16,6 @@ interface RegionalAgentCardProps {
export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onDelete }) => {
const { toast } = useToast();
const { t } = useLanguage();
// Check if this is the default agent that cannot be removed
const isDefaultAgent = agent.agent_id === "1" || agent.region_name === "Default";
@@ -26,13 +24,13 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
try {
await navigator.clipboard.writeText(agent.agent_id);
toast({
title: t('copied'),
description: t('copiedDescription'),
title: "Copied!",
description: "Agent ID copied to clipboard.",
});
} catch (error) {
toast({
title: t('copyFailed'),
description: t('copyFailedDescription'),
title: "Copy failed",
description: "Failed to copy agent ID.",
variant: "destructive",
});
}
@@ -42,14 +40,14 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
if (agent.connection === 'online') {
return {
icon: <Wifi className="h-4 w-4" />,
label: t('online'),
label: 'Online',
variant: 'default' as const,
className: 'bg-green-100 text-green-800 hover:bg-green-100'
};
} else {
return {
icon: <WifiOff className="h-4 w-4" />,
label: t('offline'),
label: 'Offline',
variant: 'secondary' as const,
className: 'bg-red-100 text-red-800 hover:bg-red-100'
};
@@ -69,7 +67,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
{agent.region_name}
{isDefaultAgent && (
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
{t('defaultBadge')}
Default
</Badge>
)}
</CardTitle>
@@ -88,12 +86,12 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={copyAgentId}>
<Copy className="mr-2 h-4 w-4" />
{t('copyAgentId')}
Copy Agent ID
</DropdownMenuItem>
{!isDefaultAgent && (
<DropdownMenuItem onClick={onDelete} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
{t('removeAgent')}
Remove Agent
</DropdownMenuItem>
)}
</DropdownMenuContent>
@@ -118,12 +116,12 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{t('agentId')}</span>
<span className="text-muted-foreground">Agent ID:</span>
<span className="font-mono text-xs">{agent.agent_id.substring(0, 12)}...</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{t('lastUpdated')}</span>
<span className="text-muted-foreground">Last Updated:</span>
<span className="text-xs">
{formatDistanceToNow(new Date(agent.updated), { addSuffix: true })}
</span>
@@ -134,7 +132,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<div className="pt-2 border-t">
<div className="flex items-center text-xs text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse"></div>
{t('activeMonitoring')}
Active monitoring
</div>
</div>
)}
@@ -143,7 +141,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<div className="pt-2 border-t">
<div className="flex items-center text-xs text-red-600">
<div className="w-2 h-2 bg-red-600 rounded-full mr-2"></div>
{t('connectionLost')}
Connection lost
</div>
</div>
)}
@@ -1,128 +0,0 @@
import React from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Copy } from "lucide-react";
import { copyToClipboard } from "@/utils/copyUtils";
import { useLanguage } from "@/contexts/LanguageContext";
interface RegionalAgentConfigFormProps {
formData: {
regionName: string;
agentIp: string;
};
setFormData: (data: { regionName: string; agentIp: string }) => void;
agentId: string;
agentToken: string;
currentPocketBaseUrl: string;
isSubmitting: boolean;
onSubmit: (e: React.FormEvent) => void;
}
export const RegionalAgentConfigForm: React.FC<RegionalAgentConfigFormProps> = ({
formData,
setFormData,
agentId,
agentToken,
currentPocketBaseUrl,
isSubmitting,
onSubmit,
}) => {
const { t } = useLanguage();
return (
<form onSubmit={onSubmit} className="space-y-6">
<div className="space-y-4">
<div>
<Label htmlFor="regionName">{t('regionName')}</Label>
<Input
id="regionName"
placeholder={t('regionNamePlaceholder')}
value={formData.regionName}
onChange={(e) => setFormData({ ...formData, regionName: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="agentIp">{t('agentServerIpAddress')}</Label>
<Input
id="agentIp"
placeholder={t('agentIpPlaceholder')}
value={formData.agentIp}
onChange={(e) => setFormData({ ...formData, agentIp: e.target.value })}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium">{t('agentId')}</Label>
<div className="relative">
<Input
value={agentId}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
/>
<Button
type="button"
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(agentId)}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div>
<Label className="text-sm font-medium">{t('apiEndpoint')}</Label>
<div className="relative">
<Input
value={currentPocketBaseUrl}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
/>
<Button
type="button"
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(currentPocketBaseUrl)}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
</div>
<div>
<Label className="text-sm font-medium">{t('authenticationToken')}</Label>
<div className="relative">
<Input
value={agentToken}
readOnly
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
/>
<Button
type="button"
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(agentToken)}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
</div>
<div className="flex justify-end space-x-2">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? t('creating') : t('createAgent')}
</Button>
</div>
</form>
);
};
@@ -1,159 +0,0 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Copy, Terminal } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
import { useLanguage } from "@/contexts/LanguageContext";
interface RegionalManualTabProps {
agentToken: string;
currentPocketBaseUrl: string;
formData: {
regionName: string;
agentIp: string;
};
agentId: string;
onDialogClose: () => void;
}
export const RegionalManualTab: React.FC<RegionalManualTabProps> = ({
agentToken,
currentPocketBaseUrl,
formData,
agentId,
onDialogClose,
}) => {
const { t } = useLanguage();
const getManualInstallSteps = () => {
return [
{
title: t('step1DownloadPackage'),
commands: [
{
label: t('downloadAmd64Notice'),
command: 'wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb'
},
{
label: t('downloadArm64Notice'),
command: 'wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb'
}
]
},
{
title: t('step2InstallPackage'),
commands: [
{
label: '',
command: 'sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb\nsudo apt-get install -f'
}
]
},
{
title: t('step3ConfigureAgent'),
commands: [
{
label: '',
command: 'sudo nano /etc/regional-check-agent/regional-check-agent.env'
},
{
label: t('addConfigurationValues'),
command: `POCKETBASE_URL=${currentPocketBaseUrl}
AGENT_TOKEN=${agentToken}
AGENT_ID=${agentId}
REGION_NAME=${formData.regionName}
AGENT_IP=${formData.agentIp}`
}
]
},
{
title: t('step4StartService'),
commands: [
{
label: '',
command: 'sudo systemctl enable regional-check-agent\nsudo systemctl start regional-check-agent'
}
]
},
{
title: t('step5VerifyInstallation'),
commands: [
{
label: '',
command: 'sudo systemctl status regional-check-agent\ncurl http://localhost:8091/health'
}
]
}
];
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
{t('manualInstallationSteps')}
</CardTitle>
<CardDescription>
{t('stepByStepManualInstallation')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
{getManualInstallSteps().map((step, stepIndex) => (
<div key={stepIndex} className="border-l-4 border-blue-500 pl-4 space-y-2">
<div className="flex items-center gap-2">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm font-medium">
{stepIndex + 1}
</span>
<p className="font-medium">{step.title}</p>
</div>
{step.commands.map((cmd, cmdIndex) => (
<div key={cmdIndex} className="space-y-1">
{cmd.label && (
<p className="text-xs text-muted-foreground ml-8">{cmd.label}</p>
)}
<div className="ml-8 relative">
<pre className="bg-muted p-3 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{cmd.command}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(cmd.command)}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
))}
</div>
))}
</div>
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">Prerequisites:</h4>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-disc list-inside mb-3">
<li>Ensure you have root/sudo access on the target server</li>
<li>Make sure wget or curl is installed for downloading files</li>
<li>Internet connection required for downloading packages</li>
</ul>
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
<p className="text-sm text-blue-800 dark:text-blue-200">
The agent will start automatically and appear in your dashboard within a few minutes.
</p>
</div>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
{t('done')}
</Button>
</div>
</CardContent>
</Card>
);
};
@@ -9,10 +9,8 @@ import { RegionalService } from "@/types/regional.types";
import { AddRegionalAgentDialog } from "./AddRegionalAgentDialog";
import { RegionalAgentCard } from "./RegionalAgentCard";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
export const RegionalMonitoringContent = () => {
const { t } = useLanguage();
const [addDialogOpen, setAddDialogOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
@@ -26,8 +24,8 @@ export const RegionalMonitoringContent = () => {
const handleAgentAdded = () => {
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({
title: t('regionalAgentAdded'),
description: t('regionalAgentAddedDesc'),
title: "Regional Agent Added",
description: "The regional monitoring agent has been successfully configured.",
});
};
@@ -36,13 +34,13 @@ export const RegionalMonitoringContent = () => {
await regionalService.deleteRegionalService(id);
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({
title: t('agentRemoved'),
description: t('agentRemovedDesc'),
title: "Agent Removed",
description: "The regional monitoring agent has been removed.",
});
} catch (error) {
toast({
title: t('error'),
description: t('failedToRemoveAgent'),
title: "Error",
description: "Failed to remove the regional monitoring agent.",
variant: "destructive",
});
}
@@ -55,14 +53,14 @@ export const RegionalMonitoringContent = () => {
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('regionalmonitoring')}</h1>
<h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
<p className="text-muted-foreground">
{t('descriptRegionPage')}
Manage distributed monitoring agents across different regions
</p>
</div>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
{t('addRegionalAgent')}
Add Regional Agent
</Button>
</div>
@@ -70,39 +68,39 @@ export const RegionalMonitoringContent = () => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('totalAgents')}</CardTitle>
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalAgents}</div>
<p className="text-xs text-muted-foreground">
{t('regionalMonitoringAgents')}
Regional monitoring agents
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('onlineAgents')}</CardTitle>
<CardTitle className="text-sm font-medium">Online Agents</CardTitle>
<Wifi className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
<p className="text-xs text-muted-foreground">
{t('currentlyConnected')}
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">{t('offlineAgents')}</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">
{t('disconnectedAgents')}
Disconnected agents
</p>
</CardContent>
</Card>
@@ -110,7 +108,7 @@ export const RegionalMonitoringContent = () => {
{/* Agents List */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">{t('regionalAgents')}</h2>
<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">
@@ -133,13 +131,13 @@ export const RegionalMonitoringContent = () => {
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">{t('noRegionalAgents')}</h3>
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
<p className="text-muted-foreground text-center mb-4">
{t('getStartedAddAgent')}
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" />
{t('addFirstAgent')}
Add First Agent
</Button>
</CardContent>
</Card>
@@ -1,126 +0,0 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Copy, Download, Zap, Play } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
import { useLanguage } from "@/contexts/LanguageContext";
import { useToast } from "@/hooks/use-toast";
interface RegionalOneClickTabProps {
agentToken: string;
currentPocketBaseUrl: string;
formData: {
regionName: string;
agentIp: string;
};
agentId: string;
onDialogClose: () => void;
}
export const RegionalOneClickTab: React.FC<RegionalOneClickTabProps> = ({
agentToken,
currentPocketBaseUrl,
formData,
agentId,
onDialogClose,
}) => {
const { t } = useLanguage();
const { toast } = useToast();
const getOneClickCommand = () => {
return `curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
--region-name="${formData.regionName}" \\
--agent-id="${agentId}" \\
--agent-ip="${formData.agentIp}" \\
--token="${agentToken}" \\
--pocketbase-url="${currentPocketBaseUrl}"`;
};
const downloadScript = () => {
const scriptContent = getOneClickCommand();
const blob = new Blob([scriptContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `install-regional-agent-${agentId}.sh`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: t('downloaded'),
description: t('downloadedDescription'),
});
};
return (
<Card className="border-green-500/20 bg-green-50/50 dark:bg-green-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
<Zap className="h-5 w-5" />
{t('oneClickAutomaticInstallation')}
</CardTitle>
<CardDescription className="text-green-600 dark:text-green-300">
{t('completeInstallationDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4 dark:bg-green-950/50 dark:border-green-800">
<div className="flex items-center gap-2 text-green-800 dark:text-green-200 font-medium mb-2">
<Play className="h-4 w-4" />
{t('whatThisScriptDoes')}
</div>
<ul className="text-sm text-green-700 dark:text-green-300 space-y-1 ml-6">
<li> {t('scriptActionDownload')}</li>
<li> {t('scriptActionInstall')}</li>
<li> {t('scriptActionConfigure')}</li>
<li> {t('scriptActionStart')}</li>
<li> {t('scriptActionHealthChecks')}</li>
</ul>
</div>
<div className="space-y-2">
<Label className="text-green-700 dark:text-green-400">{t('runCommandOnServer')}</Label>
<div className="relative">
<Textarea
readOnly
value={getOneClickCommand()}
className="font-mono text-sm min-h-[120px] pr-12 bg-muted/50 border-green-200 dark:border-green-800"
/>
<Button
type="button"
size="sm"
variant="outline"
className="absolute top-2 right-2 bg-green-50 dark:bg-green-950/50 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900"
onClick={() => copyToClipboard(getOneClickCommand())}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div className="flex gap-2">
<Button onClick={downloadScript} variant="outline" className="flex-1">
<Download className="mr-2 h-4 w-4" />
{t('downloadCompleteScript')}
</Button>
</div>
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 dark:bg-yellow-950/20 dark:border-yellow-800">
<p className="text-sm text-yellow-800 dark:text-yellow-200" dangerouslySetInnerHTML={{ __html: t('localDevelopmentNotice') }} />
</div>
)}
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
{t('done')}
</Button>
</div>
</CardContent>
</Card>
);
};
@@ -25,20 +25,19 @@ export const IncidentDetailContent = ({
onClose,
assignedUser
}: IncidentDetailContentProps) => {
// Fetch assigned user details if none was provided; prefer server field
const assigneeId = incident?.assigned_users || incident?.assigned_to;
// Fetch assigned user details if one wasn't provided and there's an assigned_to field
const { data: fetchedUser } = useQuery({
queryKey: ['user', assigneeId],
queryKey: ['user', incident?.assigned_to],
queryFn: async () => {
if (!assigneeId) return null;
if (!incident?.assigned_to) return null;
try {
return await userService.getUser(assigneeId);
return await userService.getUser(incident.assigned_to);
} catch (error) {
console.error("Failed to fetch assigned user:", error);
return null;
}
},
enabled: !!assigneeId && !assignedUser,
enabled: !!incident?.assigned_to && !assignedUser,
staleTime: 300000 // Cache for 5 minutes
});
@@ -17,7 +17,7 @@ export const IncidentDetailDialog = ({
onOpenChange,
incident
}: IncidentDetailDialogProps) => {
// Fetch user data for assigned field (prefer assigned_users, fallback to assigned_to)
// Fetch user data for assigned_to field
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_users || incident?.assigned_to) && open // Only run query if there's an assigned value and dialog is open
enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
});
// 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))
// Find the assigned user
const assignedUser = incident?.assigned_to
? users.find(user => user.id === incident.assigned_to)
: null;
if (!incident) return null;
@@ -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 field (prefer assigned_users, fallback to assigned_to)
// Fetch assigned user details if there's an assigned_to field
const { data: assignedUser } = useQuery({
queryKey: ['user', incident?.assigned_users || incident?.assigned_to],
queryKey: ['user', incident?.assigned_to],
queryFn: async () => {
if (!(incident?.assigned_users || incident?.assigned_to)) return null;
if (!incident?.assigned_to) return null;
try {
return await userService.getUser(incident?.assigned_users || incident?.assigned_to);
return await userService.getUser(incident.assigned_to);
} catch (error) {
console.error("Failed to fetch assigned user:", error);
return null;
}
},
enabled: !!(incident?.assigned_users || incident?.assigned_to),
enabled: !!incident?.assigned_to,
staleTime: 300000 // Cache for 5 minutes
});
@@ -31,8 +31,8 @@ export const AssignmentSection: React.FC<AssignmentSectionProps> = ({ incident,
</Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span>
</div>
) : (incident.assigned_users || incident.assigned_to) ? (
<span>{incident.assigned_users || incident.assigned_to}</span>
) : incident.assigned_to ? (
<span>{incident.assigned_to}</span>
) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span>
)}
@@ -50,8 +50,8 @@ export const BasicInfoSection: React.FC<BasicInfoSectionProps> = ({ incident, as
</Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span>
</div>
) : (incident.assigned_users || incident.assigned_to) ? (
<span>{incident.assigned_users || incident.assigned_to}</span>
) : incident.assigned_to ? (
<span>{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: { full_name?: string; username: string }): string => {
const getUserInitials = (user: any): 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_users || incident.assigned_to || '',
assigned_to: incident.assigned_to || '',
root_cause: incident.root_cause || '',
resolution_steps: incident.resolution_steps || '',
lessons_learned: incident.lessons_learned || '',
@@ -45,15 +45,12 @@ export const useIncidentEditForm = (
impact: data.impact,
priority: data.priority,
service_id: data.service_id,
...(data.assigned_to
? { assigned_to: data.assigned_to, assigned_users: data.assigned_to }
: {}),
assigned_to: data.assigned_to, // This is the user ID from the form
root_cause: data.root_cause,
resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned,
});
toast({
title: t('incidentUpdated'),
description: t('incidentUpdatedDesc'),
@@ -57,7 +57,6 @@ 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_users || localItem.assigned_to} />
<AssignedUserCell userId={localItem.assigned_to} />
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-end items-center space-x-2">
@@ -14,9 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getCurrentEndpoint } from "@/lib/pocketbase";
import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
import { OneClickInstallTab } from "./OneClickInstallTab";
import { DockerOneClickTab } from "./DockerOneClickTab";
import { ManualInstallTab } from "./ManualInstallTab";
import { useLanguage } from "@/contexts/LanguageContext";
interface AddServerAgentDialogProps {
open: boolean;
@@ -29,13 +27,15 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
onOpenChange,
onAgentAdded,
}) => {
const { t } = useLanguage();
const { toast } = useToast();
const queryClient = useQueryClient();
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeTab, setActiveTab] = useState("configure");
// Get current PocketBase URL
const currentPocketBaseUrl = getCurrentEndpoint();
// Form state
const [formData, setFormData] = useState({
serverName: "",
description: "",
@@ -46,46 +46,23 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
notificationEnabled: true,
});
const generateNewCredentials = () => ({
serverToken: `srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`,
serverId: `agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
});
const [serverToken, setServerToken] = useState(() =>
// Generated server token and agent ID
const [serverToken] = useState(() =>
`srv_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
);
const [serverId, setServerId] = useState(() =>
const [serverId] = useState(() =>
`agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`
);
// Refresh credentials and reset form whenever the dialog opens
React.useEffect(() => {
if (open) {
const newCredentials = generateNewCredentials();
setServerToken(newCredentials.serverToken);
setServerId(newCredentials.serverId);
setActiveTab("configure");
setFormData({
serverName: "",
description: "",
osType: "",
checkInterval: "60",
retryAttempt: "3",
dockerEnabled: false,
notificationEnabled: true,
});
}
}, [open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return;
if (!formData.serverName || !formData.osType) {
toast({
title: t('validationError'),
description: t('fillRequiredFields'),
title: "Validation Error",
description: "Please fill in all required fields.",
variant: "destructive",
});
return;
@@ -94,18 +71,22 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
setIsSubmitting(true);
try {
// Here you would typically create the server monitoring configuration
// For now, we'll simulate the process
await new Promise(resolve => setTimeout(resolve, 1500));
toast({
title: t('serverAgentCreated'),
description: t('serverAgentCreatedDesc').replace('{name}', formData.serverName),
title: "Server Agent Created",
description: `${formData.serverName} monitoring agent has been configured successfully.`,
});
// Switch to one-click install tab after successful creation
setActiveTab("one-click");
onAgentAdded();
} catch (error) {
toast({
title: t('error'),
description: t('failedToCreateAgent'),
title: "Error",
description: "Failed to create server monitoring agent.",
variant: "destructive",
});
} finally {
@@ -114,6 +95,17 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
};
const handleDialogClose = () => {
// Reset form and tab when dialog closes
setActiveTab("configure");
setFormData({
serverName: "",
description: "",
osType: "",
checkInterval: "60",
retryAttempt: "3",
dockerEnabled: false,
notificationEnabled: true,
});
onOpenChange(false);
};
@@ -123,19 +115,18 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
{t('addServerMonitoringAgent')}
Add Server Monitoring Agent
</DialogTitle>
<DialogDescription>
{t('configureAgentDesc')}
Configure a new server monitoring agent to track system metrics and performance.
</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="configure">{t('configureAgent')}</TabsTrigger>
<TabsTrigger value="one-click">{t('oneClickInstall')}</TabsTrigger>
<TabsTrigger value="docker-one-click">{t('dockerOneClick')}</TabsTrigger>
<TabsTrigger value="manual">{t('manualInstallation')}</TabsTrigger>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="configure">Configure Agent</TabsTrigger>
<TabsTrigger value="one-click">One-Click Install</TabsTrigger>
<TabsTrigger value="manual">Manual Installation</TabsTrigger>
</TabsList>
<TabsContent value="configure" className="space-y-6">
@@ -160,16 +151,6 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
/>
</TabsContent>
<TabsContent value="docker-one-click" className="space-y-6">
<DockerOneClickTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="manual" className="space-y-6">
<ManualInstallTab
serverToken={serverToken}
@@ -1,178 +0,0 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Copy, Download, Container } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerOneClickTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
retryAttempt: string;
};
serverId: string;
onDialogClose: () => void;
}
export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const { t } = useLanguage();
const getDockerOneClickCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
return `curl -L -o server-docker-agent.sh "${scriptUrl}"
chmod +x server-docker-agent.sh
SERVER_TOKEN="${serverToken}" \\
POCKETBASE_URL="${currentPocketBaseUrl}" \\
SERVER_NAME="${formData.serverName}" \\
AGENT_ID="${serverId}" \\
sudo -E bash ./server-docker-agent.sh`;
};
const getDirectDockerCommand = () => {
return `docker run -d \\
--name monitoring-agent \\
--restart unless-stopped \\
-p 8081:8081 \\
--group-add 999 \\
-e AGENT_ID="${serverId}" \\
-e SERVER_NAME="${formData.serverName}" \\
-e SERVER_TOKEN="${serverToken}" \\
-e POCKETBASE_URL="${currentPocketBaseUrl}" \\
-e POCKETBASE_ENABLED=true \\
-v /proc:/host/proc:ro \\
-v /etc:/host/etc:ro \\
-v /sys:/host/sys:ro \\
-v /:/host/root:ro \\
-v /var/run:/host/var/run:ro \\
-v /dev:/host/dev:ro \\
-v /var/run/docker.sock:/var/run/docker.sock:ro \\
-v monitoring_data:/var/lib/monitoring-agent \\
-v monitoring_logs:/var/log/monitoring-agent \\
operacle/checkcle-server-agent:latest`;
};
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy one-click command button clicked');
const command = getDockerOneClickCommand();
// console.log('Copying one-click command:', command);
await copyToClipboard(command);
};
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// console.log('Copy docker command button clicked');
const command = getDirectDockerCommand();
// console.log('Copying docker command:', command);
await copyToClipboard(command);
};
return (
<div className="space-y-6">
{/* One-Click Docker Installation */}
<Card className="border-blue-500/20 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
<Container className="h-5 w-5" />
{t('dockerOneClickTitle')}
</CardTitle>
<CardDescription className="text-blue-600 dark:text-blue-300">
{t('dockerOneClickDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-blue-700 dark:text-blue-400">{t('dockerOneClickCommand')}</Label>
<div className="relative">
<pre className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-blue-800 dark:text-blue-200">
<code>{getDockerOneClickCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2 bg-blue-50 dark:bg-blue-950/50 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-400"
onClick={handleCopyOneClickCommand}
>
<Copy className="h-4 w-4 mr-1" />
{t('copy')}
</Button>
</div>
</div>
<div className="text-sm text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 p-3 rounded-md">
<p className="font-medium mb-1">{t('dockerScriptWill')}</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>{t('dockerScriptStep1')}</li>
<li>{t('dockerScriptStep2')}</li>
<li>{t('dockerScriptStep3')}</li>
<li>{t('dockerScriptStep4')}</li>
</ol>
</div>
</CardContent>
</Card>
{/* Direct Docker Run Command */}
<Card className="border-gray-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Container className="h-5 w-5" />
{t('directDockerTitle')}
</CardTitle>
<CardDescription>
{t('directDockerDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>{t('dockerRunCommand')}</Label>
<div className="relative">
<pre className="bg-muted border p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{getDirectDockerCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={handleCopyDockerCommand}
>
<Copy className="h-4 w-4 mr-1" />
{t('copy')}
</Button>
</div>
</div>
<div className="text-sm text-muted-foreground bg-muted/50 border p-3 rounded-md">
<p className="font-medium mb-1">{t('dockerPrerequisites')}</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>{t('dockerPrereqStep1')}</li>
<li>{t('dockerPrereqStep2')}</li>
<li>{t('dockerPrereqStep3')}</li>
</ol>
</div>
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
{t('done')}
</Button>
</div>
</div>
);
};
@@ -12,9 +12,8 @@ import { pb } from "@/lib/pocketbase";
import { Server } from "@/types/server.types";
import { RefreshCw, X } from "lucide-react";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { templateService, ServerNotificationTemplate } from "@/services/templateService";
import { templateService, NotificationTemplate } from "@/services/templateService";
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
import { useLanguage } from "@/contexts/LanguageContext";
interface EditServerDialogProps {
server: Server | null;
@@ -26,7 +25,7 @@ interface EditServerDialogProps {
interface ServerFormData {
name: string;
check_interval: number;
max_retries: number;
retry_attempts: number;
docker_monitoring: boolean;
notification_enabled: boolean;
notification_channels: string[]; // Changed to array for multiple selections
@@ -47,11 +46,10 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
onOpenChange,
onServerUpdated,
}) => {
const { t } = useLanguage();
const [formData, setFormData] = useState<ServerFormData>({
name: "",
check_interval: 60,
max_retries: 3,
retry_attempts: 3,
docker_monitoring: false,
notification_enabled: false,
notification_channels: [], // Changed to array
@@ -68,9 +66,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [templates, setTemplates] = useState<ServerNotificationTemplate[]>([]);
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<ServerNotificationTemplate | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
const [loadingTemplates, setLoadingTemplates] = useState(false);
@@ -80,6 +78,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Initialize form data when server changes
useEffect(() => {
if (server) {
// console.log("Setting form data for server:", server);
// Parse comma-separated notification_id into array
const notificationChannels = server.notification_id
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
@@ -88,9 +87,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
setFormData({
name: server.name || "",
check_interval: server.check_interval || 60,
max_retries: server.max_retries || 3,
retry_attempts: 3,
docker_monitoring: server.docker === "true",
notification_enabled: server.notification_status === true,
notification_enabled: notificationChannels.length > 0,
notification_channels: notificationChannels,
threshold_id: server.threshold_id || "none",
template_id: server.template_id || "none",
@@ -110,13 +109,15 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Load existing threshold data when thresholds are loaded and we have a server with threshold_id
useEffect(() => {
if (server && server.threshold_id && thresholds.length > 0) {
// console.log("Loading existing threshold data for server:", server.threshold_id);
const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
if (existingThreshold) {
// console.log("Found existing threshold:", existingThreshold);
setSelectedThreshold(existingThreshold);
// Handle the API response format with proper field names and type conversion
setThresholdFormData({
cpu_threshold: parseInt(String(existingThreshold.cpu_threshold)) || 80,
ram_threshold: parseInt(String((existingThreshold.ram_threshold_message ?? existingThreshold.ram_threshold))) || 80,
ram_threshold: parseInt(String((existingThreshold as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80,
disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80,
network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80,
});
@@ -143,7 +144,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
// Handle the API response format with proper field names and type conversion
setThresholdFormData({
cpu_threshold: parseInt(String(threshold.cpu_threshold)) || 80,
ram_threshold: parseInt(String((threshold.ram_threshold_message ?? threshold.ram_threshold))) || 80,
ram_threshold: parseInt(String((threshold as any).ram_threshold_message || threshold.ram_threshold)) || 80,
disk_threshold: parseInt(String(threshold.disk_threshold)) || 80,
network_threshold: parseInt(String(threshold.network_threshold)) || 80,
});
@@ -165,10 +166,11 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs);
} catch (error) {
// console.error('Error loading alert configurations:', error);
toast({
variant: "destructive",
title: t('error', 'instance'),
description: t('failedToLoadChannels', 'instance'),
title: "Error",
description: "Failed to load notification channels",
});
} finally {
setLoadingAlertConfigs(false);
@@ -178,14 +180,14 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const loadTemplates = async () => {
try {
setLoadingTemplates(true);
const templateList = await templateService.getTemplates('server');
// Cast to ServerNotificationTemplate[] since we know we're getting server templates
setTemplates(templateList as ServerNotificationTemplate[]);
const templateList = await templateService.getTemplates();
setTemplates(templateList);
} catch (error) {
// console.error('Error loading templates:', error);
toast({
variant: "destructive",
title: t('error', 'instance'),
description: t('failedToLoadTemplates', 'instance'),
title: "Error",
description: "Failed to load templates",
});
} finally {
setLoadingTemplates(false);
@@ -198,16 +200,58 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
const thresholdList = await serverThresholdService.getServerThresholds();
setThresholds(thresholdList);
} catch (error) {
// console.error('Error loading server thresholds:', error);
toast({
variant: "destructive",
title: t('error', 'instance'),
description: t('failedToLoadThresholds', 'instance'),
title: "Error",
description: "Failed to load server thresholds",
});
} finally {
setLoadingThresholds(false);
}
};
const handleThresholdUpdate = async () => {
if (!selectedThreshold) return;
try {
// Use the correct field name for RAM threshold
const updateData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
toast({
title: "Threshold updated",
description: "Server threshold values have been updated successfully.",
});
} catch (error) {
// console.error('Error updating threshold:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to update threshold values.",
});
}
};
const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
setFormData(prev => ({
...prev,
@@ -232,69 +276,36 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
setIsSubmitting(true);
// Convert notification channels array to comma-separated string
const notificationChannelsString = formData.notification_channels.join(',');
const notificationChannelsString = formData.notification_enabled
? formData.notification_channels.join(',')
: "";
const updateData = {
name: formData.name,
check_interval: formData.check_interval,
max_retries: formData.max_retries,
docker: formData.docker_monitoring ? "true" : "false",
notification_status: formData.notification_enabled,
notification_id: notificationChannelsString,
threshold_id: formData.threshold_id !== "none" ? formData.threshold_id : "",
template_id: formData.template_id !== "none" ? formData.template_id : "",
threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "",
template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "",
updated: new Date().toISOString(),
};
await pb.collection('servers').update(server.id, updateData);
// Update threshold if a threshold is selected and values have been modified
if (selectedThreshold && formData.threshold_id !== "none") {
try {
const updateThresholdData = {
cpu_threshold: thresholdFormData.cpu_threshold,
ram_threshold_message: thresholdFormData.ram_threshold,
disk_threshold: thresholdFormData.disk_threshold,
network_threshold: thresholdFormData.network_threshold,
};
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateThresholdData);
// Update local state
setSelectedThreshold({
...selectedThreshold,
...thresholdFormData,
});
// Update thresholds list
setThresholds(prev => prev.map(t =>
t.id === selectedThreshold.id
? { ...t, ...thresholdFormData }
: t
));
} catch (error) {
toast({
variant: "destructive",
title: t('warning', 'instance'),
description: t('serverUpdatedButThresholdFailed', 'instance'),
});
}
}
toast({
title: t('serverUpdated', 'instance'),
description: t('serverUpdatedDesc', 'instance', { name: formData.name }),
title: "Server updated",
description: `${formData.name} has been updated successfully.`,
});
onServerUpdated();
onOpenChange(false);
} catch (error) {
// console.error('Error updating server:', error);
toast({
variant: "destructive",
title: t('error', 'instance'),
description: t('failedToUpdateServer', 'instance'),
title: "Error",
description: "Failed to update server. Please try again.",
});
} finally {
setIsSubmitting(false);
@@ -310,9 +321,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
setFormData({
name: server.name || "",
check_interval: server.check_interval || 60,
max_retries: server.max_retries || 3,
retry_attempts: 3,
docker_monitoring: server.docker === "true",
notification_enabled: server.notification_status === true,
notification_enabled: notificationChannels.length > 0,
notification_channels: notificationChannels,
threshold_id: server.threshold_id || "none",
template_id: server.template_id || "none",
@@ -325,62 +336,62 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('editServerConfiguration', 'instance')}</DialogTitle>
<DialogTitle>Edit Server Configuration</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="serverName">{t('serverNameLabel', 'instance')}</Label>
<Label htmlFor="serverName">Server Name *</Label>
<Input
id="serverName"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder={t('serverNamePlaceholder', 'instance')}
placeholder="Enter server name"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="checkInterval">{t('checkIntervalLabel', 'instance')}</Label>
<Label htmlFor="checkInterval">Check Interval</Label>
<Select
value={formData.check_interval.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, check_interval: parseInt(value) }))}
>
<SelectTrigger>
<SelectValue placeholder={t('selectInterval', 'instance')} />
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
<SelectItem value="30">{t('interval30s', 'instance')}</SelectItem>
<SelectItem value="60">{t('interval1m', 'instance')}</SelectItem>
<SelectItem value="120">{t('interval2m', 'instance')}</SelectItem>
<SelectItem value="300">{t('interval5m', 'instance')}</SelectItem>
<SelectItem value="600">{t('interval10m', 'instance')}</SelectItem>
<SelectItem value="30">30 seconds</SelectItem>
<SelectItem value="60">1 minute</SelectItem>
<SelectItem value="120">2 minutes</SelectItem>
<SelectItem value="300">5 minutes</SelectItem>
<SelectItem value="600">10 minutes</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="maxRetries">{t('maxRetriesLabel', 'instance')}</Label>
<Label htmlFor="retryAttempts">Retry Attempts</Label>
<Select
value={formData.max_retries.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, max_retries: parseInt(value) }))}
value={formData.retry_attempts.toString()}
onValueChange={(value) => setFormData(prev => ({ ...prev, retry_attempts: parseInt(value) }))}
>
<SelectTrigger>
<SelectValue placeholder={t('selectMaxRetries', 'instance')} />
<SelectValue placeholder="Select retry attempts" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">{t('retry1', 'instance')}</SelectItem>
<SelectItem value="2">{t('retry2', 'instance')}</SelectItem>
<SelectItem value="3">{t('retry3', 'instance')}</SelectItem>
<SelectItem value="5">{t('retry5', 'instance')}</SelectItem>
<SelectItem value="10">{t('retry10', 'instance')}</SelectItem>
<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="10">10 attempts</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="dockerMonitoring">{t('dockerMonitoring', 'instance')}</Label>
<Label htmlFor="dockerMonitoring">Docker Monitoring</Label>
<div className="flex items-center space-x-2">
<Switch
id="dockerMonitoring"
@@ -391,7 +402,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
}))}
/>
<Label htmlFor="dockerMonitoring" className="text-sm text-muted-foreground">
{formData.docker_monitoring ? t('enabled', 'instance') : t('disabled', 'instance')}
{formData.docker_monitoring ? "Enabled" : "Disabled"}
</Label>
</div>
</div>
@@ -405,26 +416,28 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
checked={formData.notification_enabled}
onCheckedChange={(checked) => setFormData(prev => ({
...prev,
notification_enabled: checked
// Remove the automatic clearing of notification_channels, threshold_id, and template_id
notification_enabled: checked,
notification_channels: checked ? prev.notification_channels : [],
threshold_id: checked ? prev.threshold_id : "none",
template_id: checked ? prev.template_id : "none"
}))}
/>
<Label htmlFor="notificationEnabled">{t('enableNotifications', 'instance')}</Label>
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
</div>
{/* Expanded Notification Settings */}
{formData.notification_enabled && (
<Card className="border-l-4 border-l-blue-500">
<CardHeader>
<CardTitle className="text-lg">{t('notificationSettings', 'instance')}</CardTitle>
<CardTitle className="text-lg">Notification Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Multiple Notification Channels Selection */}
<div className="space-y-2">
<Label>{t('notificationChannels', 'instance')}</Label>
<Label>Notification Channels</Label>
<div className="space-y-2 max-h-40 overflow-y-auto border rounded-md p-3">
{loadingAlertConfigs ? (
<div className="text-sm text-muted-foreground">{t('loadingChannels', 'instance')}</div>
<div className="text-sm text-muted-foreground">Loading channels...</div>
) : alertConfigs.length > 0 ? (
alertConfigs.map((config) => (
<div key={config.id} className="flex items-center space-x-2">
@@ -444,14 +457,14 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
</div>
))
) : (
<div className="text-sm text-muted-foreground">{t('noChannelsAvailable', 'instance')}</div>
<div className="text-sm text-muted-foreground">No notification channels available</div>
)}
</div>
{/* Selected Channels Display */}
{formData.notification_channels.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">{t('selectedChannels', 'instance')}</Label>
<Label className="text-sm font-medium">Selected Channels:</Label>
<div className="flex flex-wrap gap-2">
{formData.notification_channels.map((channelId) => {
const channel = alertConfigs.find(c => c.id === channelId);
@@ -480,17 +493,17 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
{/* Server Set Threshold Selection */}
<div className="space-y-2">
<Label htmlFor="thresholdId">{t('serverSetThreshold', 'instance')}</Label>
<Label htmlFor="thresholdId">Server Set Threshold</Label>
<Select
value={formData.threshold_id}
onValueChange={(value) => setFormData(prev => ({ ...prev, threshold_id: value }))}
disabled={loadingThresholds}
>
<SelectTrigger>
<SelectValue placeholder={loadingThresholds ? t('loadingThresholds', 'instance') : t('selectServerThreshold', 'instance')} />
<SelectValue placeholder={loadingThresholds ? "Loading thresholds..." : "Select server threshold"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{t('noThreshold', 'instance')}</SelectItem>
<SelectItem value="none">No threshold (use default)</SelectItem>
{thresholds.map((threshold) => (
<SelectItem key={threshold.id} value={threshold.id}>
{threshold.name}
@@ -503,13 +516,21 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
{/* Editable Threshold Details */}
{selectedThreshold && (
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-base">{t('thresholdDetails', 'instance')}: {selectedThreshold.name}</CardTitle>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
<Button
type="button"
onClick={handleThresholdUpdate}
size="sm"
variant="outline"
>
Update Thresholds
</Button>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('cpuThresholdPct', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold (%)</Label>
<Input
type="number"
min="0"
@@ -523,7 +544,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
/>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('ramThresholdPct', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold (%)</Label>
<Input
type="number"
min="0"
@@ -537,7 +558,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
/>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('diskThresholdPct', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold (%)</Label>
<Input
type="number"
min="0"
@@ -551,7 +572,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
/>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('networkThresholdPct', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">Network Threshold (%)</Label>
<Input
type="number"
min="0"
@@ -571,17 +592,17 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
{/* Server Template Selection */}
<div className="space-y-2">
<Label htmlFor="templateId">{t('serverTemplate', 'instance')}</Label>
<Label htmlFor="templateId">Server Template</Label>
<Select
value={formData.template_id}
onValueChange={(value) => setFormData(prev => ({ ...prev, template_id: value }))}
disabled={loadingTemplates}
>
<SelectTrigger>
<SelectValue placeholder={loadingTemplates ? t('loadingTemplates', 'instance') : t('selectServerTemplate', 'instance')} />
<SelectValue placeholder={loadingTemplates ? "Loading templates..." : "Select server template"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{t('noTemplate', 'instance')}</SelectItem>
<SelectItem value="none">No template (use default)</SelectItem>
{templates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
@@ -595,44 +616,32 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
{selectedTemplate && (
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-base">{t('templateDetails', 'instance')}: {selectedTemplate.name}</CardTitle>
<CardTitle className="text-base">Template Details: {selectedTemplate.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 text-sm">
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('ramMessage', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.ram_message || t('noMessageDefined', 'instance', { name: 'RAM' })}
{selectedTemplate.up_message || "No RAM threshold message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('cpuMessage', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.cpu_message || t('noMessageDefined', 'instance', { name: 'CPU' })}
{selectedTemplate.down_message || "No CPU threshold message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('diskMessage', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.disk_message || t('noMessageDefined', 'instance', { name: 'disk' })}
{selectedTemplate.incident_message || "No disk threshold message defined"}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('networkMessage', 'instance')}</Label>
<Label className="text-xs font-medium text-muted-foreground">Network Threshold Message</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.network_message || t('noMessageDefined', 'instance', { name: 'network' })}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('upMessage', 'instance')}</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.up_message || t('noMessageDefined', 'instance', { name: 'up' })}
</p>
</div>
<div>
<Label className="text-xs font-medium text-muted-foreground">{t('downMessage', 'instance')}</Label>
<p className="text-sm bg-background p-2 rounded border">
{selectedTemplate.down_message || t('noMessageDefined', 'instance', { name: 'down' })}
{selectedTemplate.maintenance_message || "No network threshold message defined"}
</p>
</div>
</div>
@@ -651,16 +660,16 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
onClick={handleCancel}
disabled={isSubmitting}
>
{t('cancel', 'instance')}
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
{t('updating', 'instance')}
Updating...
</>
) : (
t('updateServer', 'instance')
"Update Server"
)}
</Button>
</div>
@@ -669,5 +678,3 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
</Dialog>
);
};
export default EditServerDialog;
@@ -4,7 +4,6 @@ import { Button } from "@/components/ui/button";
import { Copy, Terminal } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
import { useLanguage } from "@/contexts/LanguageContext";
interface ManualInstallTabProps {
serverToken: string;
@@ -25,22 +24,20 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
serverId,
onDialogClose,
}) => {
const { t } = useLanguage();
const getManualInstallSteps = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
return [
{
title: t('downloadScript'),
title: "Download the installation script",
command: `curl -L -o server-agent.sh "${scriptUrl}"`
},
{
title: t('makeExecutable'),
title: "Make the script executable",
command: `chmod +x server-agent.sh`
},
{
title: t('runInstall'),
title: "Run the installation with your configuration",
command: `SERVER_TOKEN="${serverToken}" POCKETBASE_URL="${currentPocketBaseUrl}" SERVER_NAME="${formData.serverName}" AGENT_ID="${serverId}" sudo bash server-agent.sh`
}
];
@@ -51,26 +48,26 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
{t('manualInstallTitle')}
Manual Installation Steps
</CardTitle>
<CardDescription>
{t('manualInstallDesc')}
Step-by-step installation process
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">{t('serverName')}:</span> {formData.serverName}
<span className="font-medium">Server Name:</span> {formData.serverName}
</div>
<div>
<span className="font-medium">{t('agentId')}:</span> {serverId}
<span className="font-medium">Agent ID:</span> {serverId}
</div>
<div>
<span className="font-medium">{t('osType')}:</span> {formData.osType}
<span className="font-medium">OS Type:</span> {formData.osType}
</div>
<div>
<span className="font-medium">{t('checkInterval')}:</span> {formData.checkInterval}s
<span className="font-medium">Check Interval:</span> {formData.checkInterval}s
</div>
</div>
</div>
@@ -96,7 +93,7 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
onClick={() => copyToClipboard(step.command)}
>
<Copy className="h-4 w-4 mr-1" />
{t('copy')}
Copy
</Button>
</div>
</div>
@@ -104,22 +101,22 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
</div>
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">{t('prerequisites')}</h4>
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">Prerequisites:</h4>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-disc list-inside mb-3">
<li>{t('prereqRoot')}</li>
<li>{t('prereqCurl')}</li>
<li>{t('prereqInternet')}</li>
<li>Ensure you have root/sudo access on the target server</li>
<li>Make sure curl is installed for downloading files</li>
<li>Internet connection required for downloading script</li>
</ul>
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">{t('afterInstall')}</h4>
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
<p className="text-sm text-blue-800 dark:text-blue-200">
{t('agentWillStart')}
The agent will start automatically and appear in your dashboard within a few minutes.
</p>
</div>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
{t('done')}
Done
</Button>
</div>
</CardContent>
@@ -5,7 +5,6 @@ import { Label } from "@/components/ui/label";
import { Copy, Download } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
import { useLanguage } from "@/contexts/LanguageContext";
interface OneClickInstallTabProps {
serverToken: string;
@@ -27,10 +26,8 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
serverId,
onDialogClose,
}) => {
const { t } = useLanguage();
const getOneClickInstallCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
return `curl -L -o server-agent.sh "${scriptUrl}"
chmod +x server-agent.sh
@@ -58,15 +55,15 @@ sudo -E bash ./server-agent.sh`;
<CardHeader>
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
<Download className="h-5 w-5" />
{t('oneClickInstallTitle')}
One-Click Install
</CardTitle>
<CardDescription className="text-green-600 dark:text-green-300">
{t('oneClickInstallDesc')}
Copy and paste this single command to install the monitoring agent instantly
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-green-700 dark:text-green-400">{t('quickInstallCommand')}</Label>
<Label className="text-green-700 dark:text-green-400">Quick Install Command</Label>
<div className="relative">
<pre className="bg-black-50 dark:bg-green-100/950 border border-green-200 dark:border-green-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-green-800 dark:text-green-200">
<code>{getOneClickInstallCommand()}</code>
@@ -79,23 +76,23 @@ sudo -E bash ./server-agent.sh`;
onClick={handleCopyCommand}
>
<Copy className="h-4 w-4 mr-1" />
{t('copy')}
Copy
</Button>
</div>
</div>
<div className="text-sm text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 p-3 rounded-md">
<p className="font-medium mb-1">{t('runCommandOnServer')}</p>
<p className="font-medium mb-1">Simply run this command on your server:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>{t('sshIntoServer')}</li>
<li>{t('pasteAndRun')}</li>
<li>{t('agentInstalled')}</li>
<li>SSH into your target server</li>
<li>Paste and run the command above</li>
<li>The agent will be installed and started automatically</li>
</ol>
</div>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
{t('done')}
Done
</Button>
</div>
</CardContent>
@@ -7,7 +7,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Copy } from "lucide-react";
import { copyToClipboard } from "@/utils/copyUtils";
import { OSSelector } from "./OSSelector";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServerAgentConfigFormProps {
formData: {
@@ -44,25 +43,23 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
isSubmitting,
onSubmit,
}) => {
const { t } = useLanguage();
return (
<form onSubmit={onSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="serverName">{t('serverName')} *</Label>
<Label htmlFor="serverName">Server Name *</Label>
<Input
id="serverName"
placeholder={t('serverNamePlaceholder')}
placeholder="e.g., web-server-01"
value={formData.serverName}
onChange={(e) => setFormData(prev => ({ ...prev, serverName: e.target.value }))}
required
/>
<p className="text-xs text-muted-foreground">{t('serverNameDesc')}</p>
<p className="text-xs text-muted-foreground">What is the name or label used as the identifier</p>
</div>
<div className="space-y-2">
<Label htmlFor="serverId">{t('serverAgentId')}</Label>
<Label htmlFor="serverId">Server Agent ID</Label>
<div className="flex gap-2">
<Input
id="serverId"
@@ -79,13 +76,13 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">{t('serverAgentIdDesc')}</p>
<p className="text-xs text-muted-foreground">Auto-generated unique identifier</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>{t('operatingSystem')} *</Label>
<Label>Operating System *</Label>
<OSSelector
value={formData.osType}
onValueChange={(value) => setFormData(prev => ({ ...prev, osType: value }))}
@@ -94,45 +91,45 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="checkInterval">{t('checkInterval')}</Label>
<Label htmlFor="checkInterval">Check Interval</Label>
<Select
value={formData.checkInterval}
onValueChange={(value) => setFormData(prev => ({ ...prev, checkInterval: value }))}
>
<SelectTrigger>
<SelectValue placeholder={t('selectInterval')} />
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
<SelectItem value="30">{t('interval30s')}</SelectItem>
<SelectItem value="60">{t('interval1m')}</SelectItem>
<SelectItem value="120">{t('interval2m')}</SelectItem>
<SelectItem value="300">{t('interval5m')}</SelectItem>
<SelectItem value="30">30 seconds</SelectItem>
<SelectItem value="60">1 minute</SelectItem>
<SelectItem value="120">2 minutes</SelectItem>
<SelectItem value="300">5 minutes</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">{t('checkIntervalDesc')}</p>
<p className="text-xs text-muted-foreground">How often to check the server and metric status</p>
</div>
<div className="space-y-2">
<Label htmlFor="retryAttempt">{t('retryAttempts')}</Label>
<Label htmlFor="retryAttempt">Retry Attempts</Label>
<Select
value={formData.retryAttempt}
onValueChange={(value) => setFormData(prev => ({ ...prev, retryAttempt: value }))}
>
<SelectTrigger>
<SelectValue placeholder={t('selectRetryAttempts')} />
<SelectValue placeholder="Select retry attempts" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">{t('attempt1')}</SelectItem>
<SelectItem value="2">{t('attempt2')}</SelectItem>
<SelectItem value="3">{t('attempt3')}</SelectItem>
<SelectItem value="5">{t('attempt5')}</SelectItem>
<SelectItem value="1">1 attempt</SelectItem>
<SelectItem value="2">2 attempts</SelectItem>
<SelectItem value="3">3 attempts</SelectItem>
<SelectItem value="5">5 attempts</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">{t('retryAttemptsDesc')}</p>
<p className="text-xs text-muted-foreground">Number of retry attempts before marking as down</p>
</div>
<div className="space-y-2">
<Label>{t('serverToken')}</Label>
<Label>Server Token</Label>
<div className="flex gap-2">
<Input value={serverToken} readOnly className="font-mono text-sm bg-muted" />
<Button
@@ -144,11 +141,11 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">{t('serverTokenDesc')}</p>
<p className="text-xs text-muted-foreground">Auto-generated authentication token</p>
</div>
<div className="space-y-2">
<Label>{t('systemUrl')}</Label>
<Label>System URL</Label>
<div className="flex gap-2">
<Input value={currentPocketBaseUrl} readOnly className="font-mono text-sm bg-muted" />
<Button
@@ -160,14 +157,14 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">{t('systemUrlDesc')}</p>
<p className="text-xs text-muted-foreground">Current system API URL</p>
</div>
</div>
</div>
<div className="pt-4">
<Button type="submit" disabled={isSubmitting} className="w-full">
{isSubmitting ? t('creatingAgent') : t('createServerAgent')}
{isSubmitting ? "Creating Agent..." : "Create Server Agent"}
</Button>
</div>
</form>
@@ -8,14 +8,12 @@ import { CPUChart } from "./charts/CPUChart";
import { MemoryChart } from "./charts/MemoryChart";
import { DiskChart } from "./charts/DiskChart";
import { NetworkChart } from "./charts/NetworkChart";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServerHistoryChartsProps {
serverId: string;
}
export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
const { t } = useLanguage();
const {
timeRange,
setTimeRange,
@@ -35,15 +33,15 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
// Show skeleton loading state for better UX
if (isLoading) {
return (
return (
<div className="space-y-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
<h2 className="text-lg font-medium">Historical Performance</h2>
<div className="flex items-center gap-2 ml-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-xs text-muted-foreground">{t("loading")}</span>
<span className="text-xs text-muted-foreground">Loading...</span>
</div>
</div>
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
@@ -79,7 +77,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
<h2 className="text-lg font-medium">Historical Performance</h2>
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
</div>
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
@@ -87,9 +85,9 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<Card>
<CardContent className="flex items-center justify-center py-12">
<div className="text-center">
<p className="text-muted-foreground">{t("errorLoadingChartData")}</p>
<p className="text-muted-foreground">Error loading chart data</p>
<p className="text-xs mt-2 font-mono text-red-500">{error?.message}</p>
<p className="text-xs mt-1 text-muted-foreground">{t("serverIdTimeRange", { serverId, timeRange })}</p>
<p className="text-xs mt-1 text-muted-foreground">Server ID: {serverId} Time Range: {timeRange}</p>
</div>
</CardContent>
</Card>
@@ -103,7 +101,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
<h2 className="text-lg font-medium">Historical Performance</h2>
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
</div>
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
@@ -111,13 +109,11 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<Card>
<CardContent className="flex items-center justify-center py-12">
<div className="text-center">
<p className="text-muted-foreground">{t("noHistoricalData", { timeRange })}</p>
<p className="text-xs mt-2">{t("rawMetricsCount", { count: metrics.length })}</p>
<p className="text-xs mt-1">{t("serverIdTimeRange", { serverId, timeRange })}</p>
<p className="text-muted-foreground">No historical data available for {timeRange}</p>
<p className="text-xs mt-2">Raw metrics count: {metrics.length}</p>
<p className="text-xs mt-1">Server ID: {serverId} Time Range: {timeRange}</p>
<p className="text-xs mt-1 text-muted-foreground">
{metrics.length > 0
? t("dataExistsOutsideRange")
: t("noMetricsDataFound")}
{metrics.length > 0 ? 'Data exists but may be outside selected time range' : 'No metrics data found'}
</p>
</div>
</CardContent>
@@ -133,13 +129,13 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
<h2 className="text-lg font-medium">Historical Performance</h2>
<span className="text-xs text-muted-foreground">
({chartData.length} data points {timeRange})
{isFetching && (
<span className="inline-flex items-center gap-1 ml-2">
<Loader2 className="h-3 w-3 animate-spin" />
<span className="text-blue-500">{t("updating")}</span>
<span className="text-blue-500">Updating...</span>
</span>
)}
</span>
@@ -3,19 +3,17 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Server, Activity, AlertTriangle, Power } from "lucide-react";
import { ServerStats } from "@/types/server.types";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServerStatsCardsProps {
stats: ServerStats;
}
export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
const { t } = useLanguage();
const { theme } = useTheme();
const cards = [
{
title: t('totalServers', 'instance'),
title: "TOTAL SERVERS",
value: stats.total,
icon: Server,
gradient: theme === 'dark'
@@ -23,7 +21,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #a0522d 100%)"
},
{
title: t('onlineServers', 'instance'),
title: "ONLINE SERVERS",
value: stats.online,
icon: Activity,
gradient: theme === 'dark'
@@ -31,7 +29,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #66bb6a 100%)"
},
{
title: t('offlineServers', 'instance'),
title: "OFFLINE SERVERS",
value: stats.offline,
icon: Power,
gradient: theme === 'dark'
@@ -39,7 +37,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #ef5350 100%)"
},
{
title: t('warningServers', 'instance'),
title: "WARNING SERVERS",
value: stats.warning,
icon: AlertTriangle,
gradient: theme === 'dark'
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
@@ -17,7 +16,6 @@ import { serverService } from "@/services/serverService";
import { useToast } from "@/hooks/use-toast";
import { pb } from "@/lib/pocketbase";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServerTableProps {
servers: Server[];
@@ -26,7 +24,6 @@ interface ServerTableProps {
}
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
const { t } = useLanguage();
const { theme } = useTheme();
const [searchTerm, setSearchTerm] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -34,9 +31,6 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [pausingServers, setPausingServers] = useState<Set<string>>(new Set());
const [selectedServerIds, setSelectedServerIds] = useState<Set<string>>(new Set());
const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false);
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
const navigate = useNavigate();
const { toast } = useToast();
@@ -46,25 +40,6 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
);
const allVisibleSelected = filteredServers.length > 0 && filteredServers.every(s => selectedServerIds.has(s.id));
const someVisibleSelected = filteredServers.some(s => selectedServerIds.has(s.id)) && !allVisibleSelected;
const toggleSelectAllVisible = (checked: boolean) => {
const newSet = new Set(selectedServerIds);
if (checked) {
filteredServers.forEach(s => newSet.add(s.id));
} else {
filteredServers.forEach(s => newSet.delete(s.id));
}
setSelectedServerIds(newSet);
};
const toggleSelectOne = (serverId: string, checked: boolean) => {
const newSet = new Set(selectedServerIds);
if (checked) newSet.add(serverId); else newSet.delete(serverId);
setSelectedServerIds(newSet);
};
const handleViewDetails = (serverId: string) => {
navigate(`/server-detail/${serverId}`);
};
@@ -93,8 +68,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
await pb.collection('servers').update(serverId, updateData);
toast({
title: isPaused ? t('serverResumed') : t('serverPaused'),
description: isPaused ? t('monitoringResumed', { name: server.name }) : t('monitoringPaused', { name: server.name }),
title: isPaused ? "Server resumed" : "Server paused",
description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
});
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
@@ -106,8 +81,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
// console.error('Error updating server status:', error);
toast({
variant: "destructive",
title: t('error'),
description: isPaused ? t('resumeServerError') : t('pauseServerError'),
title: "Error",
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
});
} finally {
setPausingServers(prev => {
@@ -161,33 +136,6 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
}
};
const confirmBulkDelete = async () => {
if (selectedServerIds.size === 0 || isBulkDeleting) return;
try {
setIsBulkDeleting(true);
const ids = Array.from(selectedServerIds);
const deletions = ids.map(id => pb.collection('servers').delete(id));
const results = await Promise.allSettled(deletions);
const failed = results.filter(r => r.status === 'rejected').length;
if (failed === 0) {
toast({ title: "Servers deleted", description: `${ids.length} server(s) have been deleted.` });
} else if (failed === ids.length) {
toast({ variant: "destructive", title: "Error", description: "Failed to delete selected servers. Please try again." });
} else {
toast({ variant: "destructive", title: "Partial success", description: `Deleted ${ids.length - failed}, failed ${failed}.` });
}
onRefresh();
setSelectedServerIds(new Set());
setBulkDeleteDialogOpen(false);
} catch (_e) {
toast({ variant: "destructive", title: "Error", description: "Failed to delete selected servers. Please try again." });
} finally {
setIsBulkDeleting(false);
}
};
const CustomProgressBar = ({
value,
label,
@@ -256,12 +204,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
return (
<Card className="flex-1 flex flex-col">
<CardHeader className="flex-shrink-0">
<CardTitle>{t('servers')}</CardTitle>
<CardTitle>Servers</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center">
<div className="flex items-center justify-center h-32">
<RefreshCw className="h-6 w-6 animate-spin" />
<span className="ml-2">{t('loadingServers')}</span>
<span className="ml-2">Loading servers...</span>
</div>
</CardContent>
</Card>
@@ -273,29 +221,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<Card className="bg-transparent border-0 shadow-none">
<CardHeader className="pb-4 px-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="text-xl font-semibold">{t('servers')}</CardTitle>
<CardTitle className="text-xl font-semibold">Servers</CardTitle>
<div className="flex items-center gap-2">
<div className="relative flex-1 sm:w-64">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('searchServersPlaceholder')}
placeholder="Search servers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
{selectedServerIds.size > 0 && (
<div className="hidden sm:block text-sm text-muted-foreground mr-2">
{selectedServerIds.size} selected
</div>
)}
<Button
onClick={() => setBulkDeleteDialogOpen(true)}
variant="destructive"
disabled={selectedServerIds.size === 0}
>
Delete Selected
</Button>
<Button onClick={onRefresh} variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
@@ -305,33 +241,23 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<CardContent className="p-0">
{filteredServers.length === 0 ? (
<div className="flex items-center justify-center p-8">
<p className="text-muted-foreground">{t('noServersFound')}</p>
<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`}>
<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'} w-10`}>
<div onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={allVisibleSelected}
onCheckedChange={(v) => toggleSelectAllVisible(Boolean(v))}
aria-label="Select all"
indeterminate={someVisibleSelected}
/>
</div>
</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('name')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('OS')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('IPAddress')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('CPU')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('memory')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('disk')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('uptime')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('lastChecked')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>{t('actions')}</TableHead>
<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>
@@ -342,20 +268,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const isPaused = server.status === "paused";
const isProcessing = pausingServers.has(server.id);
const isSelected = selectedServerIds.has(server.id);
return (
<TableRow
key={server.id}
className={`hover:bg-muted/50 cursor-pointer ${isSelected ? 'bg-muted/30' : ''}`}
onClick={() => handleViewDetails(server.id)}
>
<TableCell onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={(v) => toggleSelectOne(server.id, Boolean(v))}
aria-label={`Select ${server.name}`}
/>
</TableCell>
<TableRow key={server.id} className="hover:bg-muted/50">
<TableCell className="font-medium">
<div className="truncate" title={server.name}>
{server.name}
@@ -411,11 +325,11 @@ 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">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
<span className="sr-only">{t('openMenu')}</span>
<span className="sr-only">Open menu</span>
{isProcessing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
@@ -424,44 +338,44 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
<Eye className="mr-2 h-4 w-4" />
{t('viewServerDetail')}
View Server Detail
</DropdownMenuItem>
{server.docker === 'true' && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
<DropdownMenuItem onClick={() => handleViewContainers(server.id)}>
<Activity className="mr-2 h-4 w-4" />
{t('containerMonitoring')}
Container Monitoring
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
onClick={() => handlePauseResume(server)}
disabled={isProcessing}
>
{isPaused ? (
<>
<Play className="mr-2 h-4 w-4" />
{t('resumeMonitoring')}
Resume Monitoring
</>
) : (
<>
<Pause className="mr-2 h-4 w-4" />
{t('pauseMonitoring')}
Pause Monitoring
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
<DropdownMenuItem onClick={() => handleEdit(server)}>
<Edit className="mr-2 h-4 w-4" />
{t('editServer')}
Edit Server
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
onClick={() => handleDelete(server)}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
{t('deleteServer')}
Delete Server
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -488,45 +402,25 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteServerConfirmTitle')}</AlertDialogTitle>
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteServerConfirmDesc').replace('{name}', selectedServer?.name ?? '')}
This action cannot be undone. This will permanently delete{' '}
<span className="font-semibold text-foreground">
{selectedServer?.name}
</span>{' '}
and all of its monitoring data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>
{t('cancel')}
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={isDeleting}
className="bg-red-600 text-white hover:bg-red-700"
>
{isDeleting ? t('deleting') : t('delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Bulk Delete Confirmation Dialog */}
<AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete selected servers?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete {selectedServerIds.size} server(s) and all of their monitoring data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isBulkDeleting}>
{t('cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmBulkDelete}
disabled={isBulkDeleting}
className="bg-red-600 text-white hover:bg-red-700"
>
{isBulkDeleting ? t('deleting') : 'Delete Selected'}
{isDeleting ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -8,8 +8,6 @@ import {
} from "@/components/ui/dialog";
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;
@@ -17,12 +15,7 @@ interface AddServiceDialogProps {
}
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
const { t } = useLanguage();
const queryClient = useQueryClient();
const handleSuccess = async () => {
// Immediately invalidate and refetch services data
await queryClient.invalidateQueries({ queryKey: ["services"] });
await queryClient.refetchQueries({ queryKey: ["services"] });
const handleSuccess = () => {
onOpenChange(false);
};
@@ -34,9 +27,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">{t("createNewService")}</DialogTitle>
<DialogTitle className="text-xl">Create New Service</DialogTitle>
<DialogDescription>
{t("createNewServiceDesc")}
Fill in the details to create a new service to monitor.
</DialogDescription>
</DialogHeader>
<ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
@@ -2,7 +2,6 @@
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;
@@ -11,7 +10,6 @@ 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 {
@@ -57,7 +55,7 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
<Clock className="h-4 w-4" />
)}
<span>
{isPaused ? t("pausedAt") : ""}
{isPaused ? "Paused at " : ""}
{formattedTime}
</span>
</div>
@@ -68,14 +66,14 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
>
<div className="flex flex-col gap-1 text-xs">
<div className="font-medium">
{isPaused ? t("monitoringPaused") : t("lastCheckDetails")}
{isPaused ? "Monitoring Paused" : "Last Check Details"}
</div>
<div>
{isPaused ? t("noAutomaticChecks") : t("checkedAt") + `${formattedTime}`}
{isPaused ? "No automatic checks" : `Checked at ${formattedTime}`}
</div>
{interval && !isPaused && (
<div>
{t("checkInterval")}: {formatInterval(interval)}
Check interval: {formatInterval(interval)}
</div>
)}
<div className="text-gray-400 text-[10px]">
@@ -1,7 +1,5 @@
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">
@@ -9,8 +7,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">{t("loadingServerData")}</h3>
<p className="text-muted-foreground">{t("retrievingYourInformation")}</p>
<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>
</div>
</div>
);
@@ -1,3 +1,4 @@
import React from "react";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { Header } from "@/components/dashboard/Header";
@@ -6,9 +7,11 @@ import { ServiceNotFound } from "@/components/services/ServiceNotFound";
import { Service } from "@/types/service.types";
interface ServiceDetailWrapperProps {
children?: React.ReactNode;
children: React.ReactNode;
isLoading: boolean;
service: Service | null;
sidebarCollapsed: boolean;
toggleSidebar: () => void;
currentUser: any;
handleLogout: () => void;
}
@@ -17,16 +20,20 @@ export const ServiceDetailWrapper = ({
children,
isLoading,
service,
sidebarCollapsed,
toggleSidebar,
currentUser,
handleLogout
}: ServiceDetailWrapperProps) => {
return (
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
<Sidebar />
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
{isLoading ? (
@@ -125,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
} else {
// Fetch regional agent specific data
const [regionName, agentId] = currentAgent.split("|");
// console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
// console.log(`Retrieved ${history.length} regional monitoring records`);
}
@@ -1,34 +1,78 @@
import React, { useState, useCallback } from "react";
import { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { DateRangeOption } from "../DateRangeFilter";
import { authService } from "@/services/authService";
import { ServiceDetailContent } from "../ServiceDetailContent";
import { ServiceDetailWrapper } from "./ServiceDetailWrapper";
import { useServiceData, useRealTimeUpdates } from "./hooks";
import { toast } from "@/components/ui/use-toast";
export const ServiceDetailContainer = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// Set default to 24h
const [startDate, setStartDate] = useState<Date>(() => {
const date = new Date();
date.setHours(date.getHours() - 24);
date.setHours(date.getHours() - 24); // Go back 24 hours
return date;
});
const [endDate, setEndDate] = useState<Date>(() => {
const date = new Date();
date.setMinutes(date.getMinutes() + 5);
date.setMinutes(date.getMinutes() + 5); // Add 5 minutes buffer to future
return date;
});
const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h');
// State for sidebar collapse functionality (shared with Dashboard)
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
// Check if there's a saved preference in localStorage
const saved = localStorage.getItem("sidebarCollapsed");
return saved ? JSON.parse(saved) : window.innerWidth < 768;
});
// Toggle sidebar and save preference
const toggleSidebar = useCallback(() => {
setSidebarCollapsed(prev => {
const newState = !prev;
localStorage.setItem("sidebarCollapsed", JSON.stringify(newState));
return newState;
});
}, []);
// Get current user for header
const currentUser = authService.getCurrentUser();
useEffect(() => {
// Verify user is authenticated
if (!authService.isAuthenticated()) {
toast({
variant: "destructive",
title: "Authentication required",
description: "Please log in to view service details",
});
navigate("/login");
}
// Auto-collapse sidebar on small screens
const handleResize = () => {
if (window.innerWidth < 768 && !sidebarCollapsed) {
setSidebarCollapsed(true);
localStorage.setItem("sidebarCollapsed", JSON.stringify(true));
}
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [navigate, sidebarCollapsed]);
// Handler for logout (same as Dashboard)
const handleLogout = () => {
authService.logout();
navigate("/login");
};
// Use our custom hooks
const {
service,
uptimeData,
@@ -41,6 +85,7 @@ export const ServiceDetailContainer = () => {
handleRegionalAgentChange
} = useServiceData(id, startDate, endDate);
// Set up real-time updates
useRealTimeUpdates({
serviceId: id,
startDate,
@@ -49,12 +94,18 @@ export const ServiceDetailContainer = () => {
setUptimeData
});
// Handle date range filter changes
const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => {
console.log(`ServiceDetailContainer: Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
// Update state which will trigger the useEffect in useServiceData
setStartDate(start);
setEndDate(end);
setSelectedRange(option);
// Also explicitly fetch data with the new range to ensure immediate update
if (id) {
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
fetchUptimeData(id, start, end, option, selectedRegionalAgent);
}
}, [id, fetchUptimeData, selectedRegionalAgent]);
@@ -63,10 +114,12 @@ export const ServiceDetailContainer = () => {
<ServiceDetailWrapper
isLoading={isLoading}
service={service}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
currentUser={currentUser}
handleLogout={handleLogout}
>
{service ? (
{service && (
<ServiceDetailContent
service={service}
uptimeData={uptimeData}
@@ -76,7 +129,7 @@ export const ServiceDetailContainer = () => {
selectedRegionalAgent={selectedRegionalAgent}
onRegionalAgentChange={handleRegionalAgentChange}
/>
) : null}
)}
</ServiceDetailWrapper>
);
};
@@ -11,7 +11,6 @@ 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;
@@ -20,7 +19,6 @@ interface ServiceEditDialogProps {
}
export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) {
const { t } = useLanguage();
const queryClient = useQueryClient();
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -55,9 +53,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">{t("editService")}</DialogTitle>
<DialogTitle className="text-xl">Edit Service</DialogTitle>
<DialogDescription>
{t("editServiceDesc")}
Update the details of your monitored service.
</DialogDescription>
</DialogHeader>
{open && service && (
@@ -5,6 +5,7 @@ import { Play, Pause } from "lucide-react";
import { Service } from "@/types/service.types";
import { serviceService } from "@/services/serviceService";
import { useToast } from "@/hooks/use-toast";
import { notificationService } from "@/services/notificationService";
interface ServiceMonitoringButtonProps {
service: Service;
@@ -33,8 +34,16 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
if (onStatusChange) onStatusChange("paused");
// Notification handling removed - will be handled by backend
// console.log("Service paused - notifications will be handled by backend");
// Send notification for paused status (only here, not in pauseMonitoring.ts)
if (service.alerts !== "muted") {
// console.log("Sending pause notification from UI component");
// IMPORTANT: Direct call to the notification service to ensure a message is sent
await notificationService.sendNotification({
service: service,
status: "paused",
timestamp: new Date().toISOString(),
});
}
toast({
title: "Monitoring paused",
@@ -42,7 +51,7 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
});
} else {
// Start/resume monitoring
// console.log(`Starting monitoring for service ${service.id} (${service.name})`);
// console.log(`Starting monitoring for service ${service.id} (${service.name})`);
// First ensure we update the status in the database to not be paused anymore
await serviceService.resumeMonitoring(service.id);
@@ -4,7 +4,6 @@ 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;
@@ -12,8 +11,6 @@ interface ServiceStatsCardsProps {
}
export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) {
const { t } = useLanguage();
// Calculate uptime percentage
const calculateUptimePercentage = () => {
if (uptimeData.length === 0) return 100;
@@ -112,46 +109,41 @@ 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">{t('responseTime')}</CardTitle>
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">Response Time</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">{t('lastCheckedAt').replace('{datetime}', service.lastChecked)}</p>
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Last checked at {service.lastChecked}</p>
{avgResponseTime > 0 && avgResponseTime !== service.responseTime && (
<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>
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Avg: {avgResponseTime}ms (last {upChecks.length} up checks)</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">{t('uptime')}</CardTitle>
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">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">{t('basedOnlastChecksCount').replace('{count}', String(uptimeData.length))}</p>
<p className="text-xs text-green-600 dark:text-green-200 mt-1">Based on last {uptimeData.length} checks</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>{t("totalUptime")}: {uptimeStats.totalUptimeFormatted}</span>
<span>Total uptime: {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>{t("totalDowntime")}: {uptimeStats.totalDowntimeFormatted}</span>
<span>Total downtime: {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" ?
t('upStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
:
t('downStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
}
{service.status === "up" ? "Up" : "Down"} for {uptimeStats.currentStatusDuration}
</span>
</div>
)}
@@ -161,16 +153,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">{t('monitoringSettings')}</CardTitle>
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">Monitoring Settings</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>{t('monitoringSettingsInterval').replace('{interval}', String(service.interval))}</span>
<span>Checked every {service.interval} seconds</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} {t('monitoringSettingsType')}</span>
<span>{service.type} monitoring</span>
</div>
</CardContent>
</Card>
@@ -63,9 +63,9 @@ export function ServicesPagination({
return (
<div className="flex items-center justify-between py-4 px-4 border-t border-border">
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{t("rowsPerPage") ?? "Rows per page"}:
</span>
<span className="text-sm text-muted-foreground">
{t("rowsPerPage") ?? "Rows per page"}:
</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => onPageSizeChange(parseInt(value) as PageSize)}
@@ -80,9 +80,9 @@ export function ServicesPagination({
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground whitespace-nowrap">
{totalItems > 0
? t("servicesPagination", {"startItem": startItem, "endItem": endItem, "totalItems": totalItems})
: t("servicesPaginationNoService")
{totalItems > 0
? `${startItem}-${endItem} of ${totalItems} ${t("services") || "services"}`
: `0 ${t("services") || "services"}`
}
</span>
</div>
@@ -5,7 +5,6 @@ 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;
@@ -16,7 +15,6 @@ 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(() => {
@@ -25,21 +23,19 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
return start;
}, [endDate, interval]);
// Fetch uptime data with immediate display and background updates
// Fetch uptime data with very aggressive caching to reduce API calls
const { data: uptimeData = [] } = useQuery({
queryKey: ['uptime-bar', serviceId, serviceType],
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
enabled: !!serviceId,
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
staleTime: 30000, // Data is fresh for 30 seconds
gcTime: 600000, // Keep in cache for 10 minutes
refetchInterval: 60000, // 1 minute polling
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
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
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Memoize the uptime calculation to prevent unnecessary recalculations
@@ -88,7 +84,7 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
</div>
<span className="text-xs text-muted-foreground">
{t('last20Checks')}
Last 20 checks
</span>
</TooltipProvider>
);
@@ -3,24 +3,22 @@ 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>{t('serviceName')}</FormLabel>
<FormLabel>Service Name</FormLabel>
<FormControl>
<Input
placeholder={t('serviceNameDesc')}
placeholder="Enter a descriptive name for your service"
{...field}
/>
</FormControl>
@@ -6,14 +6,12 @@ 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");
@@ -37,7 +35,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
name="interval"
render={({ field }) => (
<FormItem>
<FormLabel>{t("checkInterval")}</FormLabel>
<FormLabel>Check Interval</FormLabel>
{!isCustomInterval ? (
<FormControl>
<Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}>
@@ -45,13 +43,13 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
<SelectValue />
</SelectTrigger>
<SelectContent>
<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>
<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>
</SelectContent>
</Select>
</FormControl>
@@ -60,7 +58,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
<FormControl>
<Input
type="number"
placeholder={t("checkIntervalPlaceholder")}
placeholder="Enter interval in seconds"
value={field.value}
onChange={field.onChange}
min="10"
@@ -74,14 +72,14 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
}}
className="text-sm text-blue-600 hover:text-blue-800"
>
{t("backToPresets")}
Back to presets
</button>
</div>
)}
<FormDescription className="text-xs">
{isCustomInterval
? t("checkIntervalDescCustom")
: t("checkIntervalDesc")
? "Enter custom interval in seconds (minimum 10 seconds)"
: "How often to check the service status"
}
</FormDescription>
<FormMessage />
@@ -94,22 +92,22 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
name="retries"
render={({ field }) => (
<FormItem>
<FormLabel>{t("retryAttempts")}</FormLabel>
<FormLabel>Retry Attempts</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} value={field.value ?? "3"}>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<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>
<SelectItem value="1">1 attempt</SelectItem>
<SelectItem value="2">2 attempts</SelectItem>
<SelectItem value="3">3 attempts</SelectItem>
<SelectItem value="5">5 attempts</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription className="text-xs">
{t("retryAttemptsDesc")}
Number of retry attempts before marking as down
</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 { useServiceSchema, ServiceFormData } from "./types";
import { serviceSchema, ServiceFormData } from "./types";
import { ServiceBasicFields } from "./ServiceBasicFields";
import { ServiceTypeField } from "./ServiceTypeField";
import { ServiceConfigFields } from "./ServiceConfigFields";
@@ -13,8 +13,6 @@ import { serviceService } from "@/services/serviceService";
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;
@@ -31,12 +29,8 @@ 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>({
@@ -74,10 +68,10 @@ export function ServiceForm({
if (onSubmitStart) onSubmitStart();
try {
// console.log("Form data being submitted:", data);
console.log("Form data being submitted:", data);
const serviceData = mapFormDataToServiceData(data);
// console.log("Service data being sent:", serviceData);
console.log("Service data being sent:", serviceData);
if (isEdit && initialData) {
// Update existing service
@@ -97,16 +91,6 @@ export function ServiceForm({
});
}
// Force immediate refresh of services data
queryClient.setQueryData(["services"], (oldData: any) => {
// Invalidate the cache to force a fresh fetch
return undefined;
});
// Invalidate and refetch services query
await queryClient.invalidateQueries({ queryKey: ["services"] });
await queryClient.refetchQueries({ queryKey: ["services"] });
onSuccess();
if (!isEdit) {
form.reset();
@@ -128,23 +112,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">{t('basicInformation')}</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</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">{t('configuration')}</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
<ServiceConfigFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('regionalMonitoring')}</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
<ServiceRegionalFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('notifications')}</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
<ServiceNotificationFields form={form} />
</div>
</div>
@@ -152,7 +136,7 @@ export function ServiceForm({
<ServiceFormActions
isSubmitting={isSubmitting}
onCancel={onCancel}
submitLabel={isEdit ? t("updateService") : t("createService")}
submitLabel={isEdit ? "Update Service" : "Create Service"}
/>
</form>
</Form>
@@ -2,7 +2,6 @@
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;
@@ -15,8 +14,6 @@ export function ServiceFormActions({
onCancel,
submitLabel = "Create Service"
}: ServiceFormActionsProps) {
const {t} = useLanguage();
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!isSubmitting) {
@@ -32,7 +29,7 @@ export function ServiceFormActions({
variant="outline"
disabled={isSubmitting}
>
{t("cancel")}
Cancel
</Button>
<Button
type="submit"
@@ -41,7 +38,7 @@ export function ServiceFormActions({
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("processing")}...
Processing...
</>
) : (
submitLabel
@@ -1,3 +1,4 @@
import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
@@ -7,16 +8,13 @@ import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
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
@@ -24,18 +22,18 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
const notificationChannels = form.watch("notificationChannels") || [];
const alertTemplate = form.watch("alertTemplate");
// console.log("Current notification values:", {
// notificationStatus,
// notificationChannels,
// alertTemplate
// });
// Fetch alert configurations for notification channels
const { data: alertConfigsData } = useQuery({
queryKey: ['alertConfigs'],
queryFn: () => alertConfigService.getAlertConfigurations(),
});
// Fetch service notification templates
const { data: serviceTemplates, isLoading: isLoadingTemplates } = useQuery({
queryKey: ['serviceNotificationTemplates'],
queryFn: () => serviceNotificationTemplateService.getTemplates(),
});
// Update alert configs when data is loaded
useEffect(() => {
if (alertConfigsData) {
@@ -44,19 +42,13 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
setAlertConfigs(enabledChannels);
// Debug log to check what alert configs are loaded
// console.log("Loaded alert configurations:", enabledChannels);
}
}, [alertConfigsData]);
// Debug log for service templates
useEffect(() => {
if (serviceTemplates) {
// console.log("Loaded service notification templates:", serviceTemplates);
}
}, [serviceTemplates]);
// Log when form values change to debug
useEffect(() => {
// console.log("Notification values changed:", {
// console.log("Notification values changed:", {
// notificationStatus: form.getValues("notificationStatus"),
// notificationChannels: form.getValues("notificationChannels")
// });
@@ -90,10 +82,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">
{t("enableNotifications")}
Enable Notifications
</FormLabel>
<FormDescription>
{t("enableNotificationsDesc")}
Enable or disable notifications for this service
</FormDescription>
</div>
<FormControl>
@@ -117,11 +109,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
name="notificationChannels"
render={({ field }) => (
<FormItem>
<FormLabel>{t("notificationChannels")}</FormLabel>
<FormLabel>Notification Channels</FormLabel>
<FormDescription>
{notificationStatus === "enabled"
? t("notificationChannelsEnabledDesc")
: t("notificationChannelsDesc")}
? "Select notification channels for this service"
: "Enable notifications first to select channels"}
</FormDescription>
{/* Display selected channels as badges */}
@@ -146,7 +138,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
value="" // Always reset to empty after selection
>
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder={t("notificationChannelsPlaceholder")} />
<SelectValue placeholder="Add a notification channel" />
</SelectTrigger>
<SelectContent>
{alertConfigs
@@ -167,35 +159,35 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
control={form.control}
name="alertTemplate"
render={({ field }) => {
// console.log("Rendering alert template field with value:", field.value);
// Don't convert existing values to "default"
const displayValue = field.value || "default";
// console.log("Rendering alert template field with value:", displayValue);
return (
<FormItem>
<FormLabel>{t("alertTemplate")}</FormLabel>
<FormLabel>Alert Template</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
field.onChange(value);
console.log("Alert template changed to:", value);
field.onChange(value === "default" ? "" : value);
}}
value={field.value || ""}
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
value={displayValue}
disabled={notificationStatus !== "enabled"}
>
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder={isLoadingTemplates ? t("alertTemplateLoading") : t("alertTemplatePlaceholder")} />
<SelectValue placeholder="Select an alert template" />
</SelectTrigger>
<SelectContent>
{serviceTemplates?.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
<SelectItem value="default">Default</SelectItem>
{/* Add templates here when available */}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
{notificationStatus === "enabled"
? t("alertTemplateEnabledDesc")
: t("alertTemplateDesc")}
? "Choose a template for alert messages"
: "Enable notifications first to select template"}
</FormDescription>
</FormItem>
);
@@ -9,14 +9,12 @@ 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") || [];
@@ -81,10 +79,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" />
{t("regionalMonitoring")}
Regional Monitoring
</FormLabel>
<div className="text-sm text-muted-foreground">
{t("regionalMonitoringDesc")}
Assign this service to regional monitoring agents for distributed monitoring
</div>
</div>
<FormControl>
@@ -109,7 +107,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
name="regionalAgents"
render={({ field }) => (
<FormItem>
<FormLabel>{t("regionalAgents")}</FormLabel>
<FormLabel>Regional Agents</FormLabel>
{/* Display selected agents */}
{currentRegionalAgents.length > 0 && (
@@ -142,12 +140,12 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<SelectTrigger>
<SelectValue placeholder={
isLoading
? t("regionalAgentsLoading")
? "Loading agents..."
: availableAgents.length > 0
? t("regionalAgentsAvailablePlaceholder")
? "Select additional regional agents..."
: currentRegionalAgents.length > 0
? t("regionalAgentsAllSelected")
: t("regionalAgentsNoAvailable")
? "All available agents selected"
: "No regional agents available"
} />
</SelectTrigger>
</FormControl>
@@ -156,14 +154,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" />
t("regionalAgentsLoading")
Loading agents...
</div>
</SelectItem>
) : availableAgents.length === 0 ? (
<SelectItem value="no-agents" disabled>
{currentRegionalAgents.length > 0
? t("regionalAgentsAllSelected")
: t("regionalAgentsNoOnlineAvailable")
? "All available agents selected"
: "No online regional agents available"
}
</SelectItem>
) : (
@@ -192,13 +190,13 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
<p className="text-sm text-amber-600">
{t("regionalAgentsNotFoundMessage")}
No online regional agents found. Services will use default monitoring.
</p>
)}
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
<p className="text-sm text-orange-600">
{t("regionalAgentsNotSelectedMessage")}
No regional agents selected. Service will use default monitoring.
</p>
)}
@@ -4,14 +4,12 @@ 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":
@@ -33,7 +31,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("serviceType")}</FormLabel>
<FormLabel>Service Type</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
@@ -58,7 +56,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>HTTP/S</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{t("serviceTypeHTTPDesc")}
Monitor websites and REST APIs with HTTP/HTTPS Protocol
</p>
</div>
</SelectItem>
@@ -69,7 +67,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>PING</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{t("serviceTypePINGDesc")}
Monitor host availability with PING Protocol
</p>
</div>
</SelectItem>
@@ -80,7 +78,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>TCP</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{t("serviceTypeTCPDesc")}
Monitor TCP port connectivity with TCP Protocol
</p>
</div>
</SelectItem>
@@ -91,7 +89,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>DNS</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{t("serviceTypeDNSDesc")}
Monitor DNS resolution
</p>
</div>
</SelectItem>
@@ -3,14 +3,12 @@ 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 = () => {
@@ -24,35 +22,35 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
case "dns":
return "example.com";
default:
return t("targetDefaultPlaceholder");
return "Enter URL or hostname";
}
};
const getDescription = () => {
switch (serviceType) {
case "http":
return t("targetHTTPDesc");
return "Enter the full URL including protocol (http:// or https://)";
case "ping":
return t("targetPINGDesc");
return "Enter hostname or IP address to ping";
case "tcp":
return t("targetTCPDesc");
return "Enter hostname or IP address for TCP connection test";
case "dns":
return t("targetDNSDesc");
return "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)";
default:
return t("targetDefaultDesc");
return "Enter the target URL or hostname for monitoring";
}
};
const getFieldLabel = () => {
switch (serviceType) {
case "dns":
return t("targetDNS");
return "Domain Name";
case "ping":
return "Hostname/IP";
case "tcp":
return "Hostname/IP";
default:
return t("targetDefault");
return "Target URL/Host";
}
};
@@ -69,7 +67,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
placeholder={getPlaceholder()}
{...field}
onChange={(e) => {
// console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
field.onChange(e);
}}
/>
@@ -95,13 +93,13 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
type="number"
{...field}
onChange={(e) => {
// console.log("Port field changed:", e.target.value);
console.log("Port field changed:", e.target.value);
field.onChange(e);
}}
/>
</FormControl>
<FormDescription className="text-xs">
{t("targetTCPPortDesc")}
Enter the port number for TCP connection test
</FormDescription>
<FormMessage />
</FormItem>
@@ -74,6 +74,7 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
notificationChannels.push(...parsedChannels);
}
} catch (error) {
// console.warn("Failed to parse notification_channel JSON:", error);
// If parsing fails, treat as single channel ID
notificationChannels.push(service.notification_channel);
}
@@ -90,13 +91,16 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
}
}
// Handle notification_status - it can be boolean or string
let notificationStatus: "enabled" | "disabled" = "disabled";
if (typeof service.notification_status === "boolean") {
notificationStatus = service.notification_status ? "enabled" : "disabled";
} else if (typeof service.notification_status === "string") {
notificationStatus = service.notification_status === "enabled" ? "enabled" : "disabled";
}
// console.log("Mapping service to form data:", {
// serviceName: service.name,
// notification_status: service.notification_status,
// notification_channel: service.notification_channel,
// notificationChannel: service.notificationChannel,
// mappedChannels: notificationChannels,
// regionalAgents: regionalAgents,
// region_name: service.region_name,
/// agent_id: service.agent_id
// });
return {
name: service.name || "",
@@ -105,7 +109,7 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
port: portValue,
interval: String(service.interval || 60),
retries: String(service.retries || 3),
notificationStatus: notificationStatus,
notificationStatus: service.notification_status || "disabled",
notificationChannels: notificationChannels,
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
regionalMonitoringEnabled: isRegionalEnabled,
@@ -148,8 +152,7 @@ export const mapFormDataToServiceData = (data: ServiceFormData) => {
type: data.type,
interval: parseInt(data.interval),
retries: parseInt(data.retries),
// Convert string status to boolean for notification_status field
notificationStatus: data.notificationStatus === "enabled",
notificationStatus: data.notificationStatus || "disabled",
notificationChannels: data.notificationChannels || [],
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
// Use regional_status field and store multiple agents as comma-separated values
@@ -1,46 +1,22 @@
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 = {
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) => !/\s/.test(value),
t("spacesNotAllowed")
)
.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(),
});
};
export type ServiceFormData = z.infer<typeof serviceSchema>;

Some files were not shown because too many files have changed in this diff Show More