feat: Add create instance monitoring button
- Adds a button to the Instance Monitoring dashboard to create server monitoring instances - Include the one-click install command in the Instance Monitoring form - Display OS list for select with images and names.
This commit is contained in:
@@ -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<AddServerAgentDialogProps> = ({
|
||||||
|
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 (
|
||||||
|
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||||
|
<DialogContent className="sm:max-w-[900px] max-w-[95vw] max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Server className="h-5 w-5" />
|
||||||
|
Add Server Monitoring Agent
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
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-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">
|
||||||
|
<ServerAgentConfigForm
|
||||||
|
formData={formData}
|
||||||
|
setFormData={setFormData}
|
||||||
|
serverId={serverId}
|
||||||
|
serverToken={serverToken}
|
||||||
|
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="one-click" className="space-y-6">
|
||||||
|
<OneClickInstallTab
|
||||||
|
serverToken={serverToken}
|
||||||
|
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||||
|
formData={formData}
|
||||||
|
serverId={serverId}
|
||||||
|
onDialogClose={handleDialogClose}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="manual" className="space-y-6">
|
||||||
|
<ManualInstallTab
|
||||||
|
serverToken={serverToken}
|
||||||
|
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||||
|
formData={formData}
|
||||||
|
serverId={serverId}
|
||||||
|
onDialogClose={handleDialogClose}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<ManualInstallTabProps> = ({
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Terminal className="h-5 w-5" />
|
||||||
|
Manual Installation Steps
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
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">Server Name:</span> {formData.serverName}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Agent ID:</span> {serverId}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">OS Type:</span> {formData.osType}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Check Interval:</span> {formData.checkInterval}s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{getManualInstallSteps().map((step, index) => (
|
||||||
|
<div key={index} className="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">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">{step.title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-8 relative">
|
||||||
|
<pre className="bg-muted p-3 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
|
<code>{step.command}</code>
|
||||||
|
</pre>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={() => copyToClipboard(step.command)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-1" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</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 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">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}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<OSSelectorProps> = ({ value, onValueChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{osOptions.map((os) => (
|
||||||
|
<button
|
||||||
|
key={os.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onValueChange(os.value)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between w-full rounded-md border px-4 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground transition",
|
||||||
|
value === os.value ? "bg-accent text-accent-foreground border-ring" : "border-input"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={os.logo}
|
||||||
|
alt={`${os.name} logo`}
|
||||||
|
className="w-6 h-6 object-contain"
|
||||||
|
/>
|
||||||
|
<span>{os.name}</span>
|
||||||
|
</div>
|
||||||
|
{value === os.value && <Check className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<OneClickInstallTabProps> = ({
|
||||||
|
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 (
|
||||||
|
<Card className="border-green-500/20 bg-green-0/50 dark:bg-green-950/20">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
|
||||||
|
<Download className="h-5 w-5" />
|
||||||
|
One-Click Install
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-green-600 dark:text-green-300">
|
||||||
|
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">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>
|
||||||
|
</pre>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
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 text-green-700 dark:text-green-400"
|
||||||
|
onClick={handleCopyCommand}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-1" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 p-3 rounded-md">
|
||||||
|
<p className="font-medium mb-1">Simply run this command on your server:</p>
|
||||||
|
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||||
|
<li>SSH into your target server</li>
|
||||||
|
<li>Paste and run the command above</li>
|
||||||
|
<li>The agent will be installed and started automatically</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button onClick={onDialogClose}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<React.SetStateAction<{
|
||||||
|
serverName: string;
|
||||||
|
description: string;
|
||||||
|
osType: string;
|
||||||
|
checkInterval: string;
|
||||||
|
retryAttempt: string;
|
||||||
|
dockerEnabled: boolean;
|
||||||
|
notificationEnabled: boolean;
|
||||||
|
}>>;
|
||||||
|
serverId: string;
|
||||||
|
serverToken: string;
|
||||||
|
currentPocketBaseUrl: string;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||||
|
formData,
|
||||||
|
setFormData,
|
||||||
|
serverId,
|
||||||
|
serverToken,
|
||||||
|
currentPocketBaseUrl,
|
||||||
|
isSubmitting,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="serverName">Server Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="serverName"
|
||||||
|
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">What is the name or label used as the identifier</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="serverId">Server Agent ID</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="serverId"
|
||||||
|
value={serverId}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(serverId)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<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>Operating System *</Label>
|
||||||
|
<OSSelector
|
||||||
|
value={formData.osType}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, osType: value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="checkInterval">Check Interval</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.checkInterval}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, checkInterval: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select interval" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="30">30 seconds</SelectItem>
|
||||||
|
<SelectItem value="60">1 minute</SelectItem>
|
||||||
|
<SelectItem value="120">2 minutes</SelectItem>
|
||||||
|
<SelectItem value="300">5 minutes</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<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">Retry Attempts</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.retryAttempt}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, retryAttempt: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select retry attempts" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">1 attempt</SelectItem>
|
||||||
|
<SelectItem value="2">2 attempts</SelectItem>
|
||||||
|
<SelectItem value="3">3 attempts</SelectItem>
|
||||||
|
<SelectItem value="5">5 attempts</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">Number of retry attempts before marking as down</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Server Token</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input value={serverToken} readOnly className="font-mono text-sm bg-muted" />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(serverToken)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Auto-generated authentication token</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>System URL</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input value={currentPocketBaseUrl} readOnly className="font-mono text-sm bg-muted" />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(currentPocketBaseUrl)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<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 ? "Creating Agent..." : "Create Server Agent"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,11 +7,14 @@ import { Sidebar } from "@/components/dashboard/Sidebar";
|
|||||||
import { Header } from "@/components/dashboard/Header";
|
import { Header } from "@/components/dashboard/Header";
|
||||||
import { ServerStatsCards } from "@/components/servers/ServerStatsCards";
|
import { ServerStatsCards } from "@/components/servers/ServerStatsCards";
|
||||||
import { ServerTable } from "@/components/servers/ServerTable";
|
import { ServerTable } from "@/components/servers/ServerTable";
|
||||||
|
import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog";
|
||||||
import { serverService } from "@/services/serverService";
|
import { serverService } from "@/services/serverService";
|
||||||
import { Server, ServerStats } from "@/types/server.types";
|
import { Server, ServerStats } from "@/types/server.types";
|
||||||
import { useSidebar } from "@/contexts/SidebarContext";
|
import { useSidebar } from "@/contexts/SidebarContext";
|
||||||
import { authService } from "@/services/authService";
|
import { authService } from "@/services/authService";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
const InstanceMonitoring = () => {
|
const InstanceMonitoring = () => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -27,6 +30,7 @@ const InstanceMonitoring = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||||
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
|
|
||||||
const { data: servers = [], isLoading, error, refetch } = useQuery({
|
const { data: servers = [], isLoading, error, refetch } = useQuery({
|
||||||
queryKey: ['servers'],
|
queryKey: ['servers'],
|
||||||
@@ -49,6 +53,10 @@ const InstanceMonitoring = () => {
|
|||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAgentAdded = () => {
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||||
@@ -102,6 +110,10 @@ const InstanceMonitoring = () => {
|
|||||||
Monitor and manage your server instances in real-time
|
Monitor and manage your server instances in real-time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={() => setAddDialogOpen(true)}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Server Agent
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,6 +129,12 @@ const InstanceMonitoring = () => {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AddServerAgentDialog
|
||||||
|
open={addDialogOpen}
|
||||||
|
onOpenChange={setAddDialogOpen}
|
||||||
|
onAgentAdded={handleAgentAdded}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user