diff --git a/application/src/components/servers/AddServerAgentDialog.tsx b/application/src/components/servers/AddServerAgentDialog.tsx new file mode 100644 index 0000000..755bb3d --- /dev/null +++ b/application/src/components/servers/AddServerAgentDialog.tsx @@ -0,0 +1,167 @@ + +import React, { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { Server } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { getCurrentEndpoint } from "@/lib/pocketbase"; +import { ServerAgentConfigForm } from "./ServerAgentConfigForm"; +import { OneClickInstallTab } from "./OneClickInstallTab"; +import { ManualInstallTab } from "./ManualInstallTab"; + +interface AddServerAgentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onAgentAdded: () => void; +} + +export const AddServerAgentDialog: React.FC = ({ + open, + onOpenChange, + onAgentAdded, +}) => { + 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: "", + osType: "", + checkInterval: "60", + retryAttempt: "3", + dockerEnabled: false, + notificationEnabled: true, + }); + + // 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] = useState(() => + `agent_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}` + ); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (isSubmitting) return; + + if (!formData.serverName || !formData.osType) { + toast({ + title: "Validation Error", + description: "Please fill in all required fields.", + variant: "destructive", + }); + return; + } + + setIsSubmitting(true); + + try { + // Here you would typically create the server monitoring configuration + // For now, we'll simulate the process + await new Promise(resolve => setTimeout(resolve, 1500)); + + toast({ + title: "Server Agent Created", + description: `${formData.serverName} monitoring agent has been configured successfully.`, + }); + + // Switch to one-click install tab after successful creation + setActiveTab("one-click"); + onAgentAdded(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to create server monitoring agent.", + variant: "destructive", + }); + } finally { + setIsSubmitting(false); + } + }; + + 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); + }; + + return ( + + + + + + Add Server Monitoring Agent + + + Configure a new server monitoring agent to track system metrics and performance. + + + + + + Configure Agent + One-Click Install + Manual Installation + + + + + + + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/application/src/components/servers/ManualInstallTab.tsx b/application/src/components/servers/ManualInstallTab.tsx new file mode 100644 index 0000000..9d289a8 --- /dev/null +++ b/application/src/components/servers/ManualInstallTab.tsx @@ -0,0 +1,125 @@ + +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"; + +interface ManualInstallTabProps { + serverToken: string; + currentPocketBaseUrl: string; + formData: { + serverName: string; + osType: string; + checkInterval: string; + }; + serverId: string; + onDialogClose: () => void; +} + +export const ManualInstallTab: React.FC = ({ + serverToken, + currentPocketBaseUrl, + formData, + serverId, + onDialogClose, +}) => { + const getManualInstallSteps = () => { + const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; + + return [ + { + title: "Download the installation script", + command: `curl -L -o server-agent.sh "${scriptUrl}"` + }, + { + title: "Make the script executable", + command: `chmod +x server-agent.sh` + }, + { + 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` + } + ]; + }; + + return ( + + + + + Manual Installation Steps + + + Step-by-step installation process + + + +
+
+
+ Server Name: {formData.serverName} +
+
+ Agent ID: {serverId} +
+
+ OS Type: {formData.osType} +
+
+ Check Interval: {formData.checkInterval}s +
+
+
+ +
+ {getManualInstallSteps().map((step, index) => ( +
+
+ + {index + 1} + + {step.title} +
+
+
+                  {step.command}
+                
+ +
+
+ ))} +
+ +
+

Prerequisites:

+
    +
  • Ensure you have root/sudo access on the target server
  • +
  • Make sure curl is installed for downloading files
  • +
  • Internet connection required for downloading script
  • +
+ +

After Installation:

+

+ The agent will start automatically and appear in your dashboard within a few minutes. +

+
+ +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/OSSelector.tsx b/application/src/components/servers/OSSelector.tsx new file mode 100644 index 0000000..6147997 --- /dev/null +++ b/application/src/components/servers/OSSelector.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface OSOption { + value: string; + name: string; + logo: string; // now it's a path to image, e.g., /logos/ubuntu.svg +} + +const osOptions: OSOption[] = [ + { value: "ubuntu", name: "Ubuntu", logo: "/upload/os/ubuntu.png" }, + { value: "debian", name: "Debian", logo: "/upload/os/debian.png" }, + { value: "centos", name: "CentOS", logo: "/upload/os/centos.png" }, + { value: "rhel", name: "Red Hat Enterprise Linux", logo: "/upload/os/rhel.png" }, + { value: "linux", name: "Linux (Generic)", logo: "/upload/os/linux.png" }, + { value: "windows", name: "Windows Server", logo: "/upload/os/windows.png" }, +]; + +interface OSSelectorProps { + value: string; + onValueChange: (value: string) => void; +} + +export const OSSelector: React.FC = ({ value, onValueChange }) => { + return ( +
+ {osOptions.map((os) => ( + + ))} +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx new file mode 100644 index 0000000..767e73e --- /dev/null +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -0,0 +1,101 @@ + +import React from "react"; +import { Button } from "@/components/ui/button"; +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"; + +interface OneClickInstallTabProps { + serverToken: string; + currentPocketBaseUrl: string; + formData: { + serverName: string; + osType: string; + checkInterval: string; + retryAttempt: string; + }; + serverId: string; + onDialogClose: () => void; +} + +export const OneClickInstallTab: React.FC = ({ + serverToken, + currentPocketBaseUrl, + formData, + serverId, + onDialogClose, +}) => { + const getOneClickInstallCommand = () => { + const scriptUrl = "https://raw.githubusercontent.com/operacle/checke-server-agent/refs/heads/main/server-agent.sh"; + + return `curl -L -o server-agent.sh "${scriptUrl}" +chmod +x server-agent.sh +SERVER_TOKEN="${serverToken}" \\ +POCKETBASE_URL="${currentPocketBaseUrl}" \\ +SERVER_NAME="${formData.serverName}" \\ +AGENT_ID="${serverId}" \\ +OS_TYPE="${formData.osType}" \\ +CHECK_INTERVAL="${formData.checkInterval}" \\ +RETRY_ATTEMPTS="${formData.retryAttempt}" \\ +sudo -E bash ./server-agent.sh`; + }; + + const handleCopyCommand = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Copy button clicked'); // Debug log + const command = getOneClickInstallCommand(); + console.log('Copying command:', command); // Debug log + await copyToClipboard(command); + }; + + return ( + + + + + One-Click Install + + + Copy and paste this single command to install the monitoring agent instantly + + + +
+ +
+
+              {getOneClickInstallCommand()}
+            
+ +
+
+ +
+

Simply run this command on your server:

+
    +
  1. SSH into your target server
  2. +
  3. Paste and run the command above
  4. +
  5. The agent will be installed and started automatically
  6. +
+
+ +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/ServerAgentConfigForm.tsx b/application/src/components/servers/ServerAgentConfigForm.tsx new file mode 100644 index 0000000..da2e858 --- /dev/null +++ b/application/src/components/servers/ServerAgentConfigForm.tsx @@ -0,0 +1,172 @@ + +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Copy } from "lucide-react"; +import { copyToClipboard } from "@/utils/copyUtils"; +import { OSSelector } from "./OSSelector"; + +interface ServerAgentConfigFormProps { + formData: { + serverName: string; + description: string; + osType: string; + checkInterval: string; + retryAttempt: string; + dockerEnabled: boolean; + notificationEnabled: boolean; + }; + setFormData: React.Dispatch>; + serverId: string; + serverToken: string; + currentPocketBaseUrl: string; + isSubmitting: boolean; + onSubmit: (e: React.FormEvent) => void; +} + +export const ServerAgentConfigForm: React.FC = ({ + formData, + setFormData, + serverId, + serverToken, + currentPocketBaseUrl, + isSubmitting, + onSubmit, +}) => { + return ( +
+
+
+ + setFormData(prev => ({ ...prev, serverName: e.target.value }))} + required + /> +

What is the name or label used as the identifier

+
+ +
+ +
+ + +
+

Auto-generated unique identifier

+
+
+ +
+
+ + setFormData(prev => ({ ...prev, osType: value }))} + /> +
+ +
+
+ + +

How often to check the server and metric status

+
+ +
+ + +

Number of retry attempts before marking as down

+
+ +
+ +
+ + +
+

Auto-generated authentication token

+
+ +
+ +
+ + +
+

Current system API URL

+
+
+
+ +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/application/src/pages/InstanceMonitoring.tsx b/application/src/pages/InstanceMonitoring.tsx index 7ad1fc4..1a9c628 100644 --- a/application/src/pages/InstanceMonitoring.tsx +++ b/application/src/pages/InstanceMonitoring.tsx @@ -7,11 +7,14 @@ import { Sidebar } from "@/components/dashboard/Sidebar"; import { Header } from "@/components/dashboard/Header"; import { ServerStatsCards } from "@/components/servers/ServerStatsCards"; import { ServerTable } from "@/components/servers/ServerTable"; +import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog"; import { serverService } from "@/services/serverService"; import { Server, ServerStats } from "@/types/server.types"; import { useSidebar } from "@/contexts/SidebarContext"; import { authService } from "@/services/authService"; import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; const InstanceMonitoring = () => { const { theme } = useTheme(); @@ -27,6 +30,7 @@ const InstanceMonitoring = () => { }); const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); + const [addDialogOpen, setAddDialogOpen] = useState(false); const { data: servers = [], isLoading, error, refetch } = useQuery({ queryKey: ['servers'], @@ -48,6 +52,10 @@ const InstanceMonitoring = () => { authService.logout(); navigate('/login'); }; + + const handleAgentAdded = () => { + refetch(); + }; if (error) { return ( @@ -102,6 +110,10 @@ const InstanceMonitoring = () => { Monitor and manage your server instances in real-time

+ @@ -117,6 +129,12 @@ const InstanceMonitoring = () => { + + ); }; diff --git a/application/src/utils/copyUtils.ts b/application/src/utils/copyUtils.ts new file mode 100644 index 0000000..24483d4 --- /dev/null +++ b/application/src/utils/copyUtils.ts @@ -0,0 +1,60 @@ + +import { toast } from "@/hooks/use-toast"; + +export const copyToClipboard = async (text: string) => { + console.log('copyToClipboard called with text:', text); // Debug log + + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + console.log('Using modern clipboard API'); // Debug log + await navigator.clipboard.writeText(text); + toast({ + title: "Copied!", + description: "Content copied to clipboard successfully.", + }); + return; + } + + console.log('Using fallback clipboard method'); // Debug log + + // Fallback for older browsers or non-secure contexts + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + textArea.style.top = "-9999px"; + textArea.style.opacity = "0"; + textArea.style.pointerEvents = "none"; + textArea.style.zIndex = "-1"; + document.body.appendChild(textArea); + + // Focus and select the text + textArea.focus(); + textArea.select(); + textArea.setSelectionRange(0, textArea.value.length); + + // Use execCommand as fallback + const successful = document.execCommand('copy'); + document.body.removeChild(textArea); + + if (successful) { + console.log('Copy successful with execCommand'); // Debug log + toast({ + title: "Copied!", + description: "Content copied to clipboard successfully.", + }); + } else { + throw new Error('Copy command failed'); + } + } catch (error) { + console.error('Failed to copy to clipboard:', error); + + // Show error toast + toast({ + title: "Copy Failed", + description: "Unable to copy automatically. Please select and copy the text manually.", + variant: "destructive", + }); + } +}; \ No newline at end of file