refactor: update the Regional Monitoring Agent dialog structure with tabs and proper form layout.
This commit is contained in:
@@ -1,16 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
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 { 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 { useToast } from "@/hooks/use-toast";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
import { getCurrentEndpoint } from "@/lib/pocketbase";
|
||||||
|
import { RegionalAgentConfigForm } from "./RegionalAgentConfigForm";
|
||||||
|
import { RegionalOneClickTab } from "./RegionalOneClickTab";
|
||||||
|
import { RegionalManualTab } from "./RegionalManualTab";
|
||||||
|
|
||||||
interface AddRegionalAgentDialogProps {
|
interface AddRegionalAgentDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -24,26 +20,49 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
|||||||
onAgentAdded
|
onAgentAdded
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
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();
|
const { toast } = useToast();
|
||||||
|
const [activeTab, setActiveTab] = useState("configure");
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
regionName: "",
|
||||||
|
agentIp: "",
|
||||||
|
});
|
||||||
|
const [agentToken, setAgentToken] = useState("");
|
||||||
|
const [agentId, setAgentId] = useState("");
|
||||||
|
const [currentPocketBaseUrl, setCurrentPocketBaseUrl] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleCreateAgent = async (e: React.FormEvent) => {
|
// Generate new credentials when dialog opens or after successful creation
|
||||||
|
const generateNewCredentials = () => {
|
||||||
|
const newToken = `rgn_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
const newAgentId = `regional_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
setAgentToken(newToken);
|
||||||
|
setAgentId(newAgentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
const endpoint = getCurrentEndpoint();
|
||||||
|
setCurrentPocketBaseUrl(endpoint);
|
||||||
|
generateNewCredentials();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!regionName.trim() || !agentIp.trim()) return;
|
if (!formData.regionName.trim() || !formData.agentIp.trim()) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const result = await regionalService.createRegionalService({
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
region_name: regionName,
|
|
||||||
agent_ip_address: agentIp,
|
|
||||||
});
|
|
||||||
|
|
||||||
setInstallCommand(result.installCommand);
|
toast({
|
||||||
setStep(2);
|
title: t('success'),
|
||||||
|
description: t('agentCreatedSuccessfully'),
|
||||||
|
});
|
||||||
|
setActiveTab("one-click");
|
||||||
|
generateNewCredentials();
|
||||||
|
|
||||||
|
onAgentAdded();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: t('error'),
|
title: t('error'),
|
||||||
@@ -55,110 +74,18 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = async (text: string, description: string = "Content") => {
|
const handleDialogClose = () => {
|
||||||
try {
|
setFormData({
|
||||||
// Try the modern clipboard API first
|
regionName: "",
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
agentIp: "",
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
toast({
|
|
||||||
title: t('copied'),
|
|
||||||
description: `${description} copied to clipboard.`,
|
|
||||||
});
|
});
|
||||||
return;
|
setActiveTab("configure");
|
||||||
}
|
generateNewCredentials();
|
||||||
|
|
||||||
// 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: t('copied'),
|
|
||||||
description: `${description} copied to clipboard.`,
|
|
||||||
//description: t('copiedDescription', { description }),
|
|
||||||
});
|
|
||||||
} 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 = t('copyFailedDescription');
|
|
||||||
if (isLocalhost) {
|
|
||||||
errorMessage += " " + t('copyFailedLocalhost');
|
|
||||||
} else if (userAgent.includes('chrome')) {
|
|
||||||
errorMessage += " " + t('copyFailedChrome');
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: t('copyFailed'),
|
|
||||||
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: t('downloaded'),
|
|
||||||
description: t('downloadedDescription'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComplete = () => {
|
|
||||||
onAgentAdded();
|
|
||||||
setStep(1);
|
|
||||||
setRegionName("");
|
|
||||||
setAgentIp("");
|
|
||||||
setInstallCommand(null);
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetDialog = () => {
|
|
||||||
setStep(1);
|
|
||||||
setRegionName("");
|
|
||||||
setAgentIp("");
|
|
||||||
setInstallCommand(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||||
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
|
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('addRegionalMonitoringAgent')}</DialogTitle>
|
<DialogTitle>{t('addRegionalMonitoringAgent')}</DialogTitle>
|
||||||
@@ -167,293 +94,45 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{step === 1 && (
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<form onSubmit={handleCreateAgent} className="space-y-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="regionName">{t('regionName')}</Label>
|
|
||||||
<Input
|
|
||||||
id="regionName"
|
|
||||||
placeholder={t('regionNamePlaceholder')}
|
|
||||||
value={regionName}
|
|
||||||
onChange={(e) => setRegionName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="agentIp">{t('agentServerIpAddress')}</Label>
|
|
||||||
<Input
|
|
||||||
id="agentIp"
|
|
||||||
placeholder={t('agentIpPlaceholder')}
|
|
||||||
value={agentIp}
|
|
||||||
onChange={(e) => setAgentIp(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
|
||||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? t('generating') : t('generateInstallation')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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">{t('agentConfigurationReady')}</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{t('oneClickInstallScriptGenerated')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue="oneclicK" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="oneclicK">{t('oneClickInstallTab')}</TabsTrigger>
|
<TabsTrigger value="configure">{t('configureAgent')}</TabsTrigger>
|
||||||
<TabsTrigger value="details">{t('agentDetailsTab')}</TabsTrigger>
|
<TabsTrigger value="one-click">{t('oneClickInstallTab')}</TabsTrigger>
|
||||||
<TabsTrigger value="manual">{t('manualInstallTab')}</TabsTrigger>
|
<TabsTrigger value="manual">{t('manualInstallTab')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="oneclicK" className="space-y-4">
|
<TabsContent value="configure" className="space-y-6">
|
||||||
<Card>
|
<RegionalAgentConfigForm
|
||||||
<CardHeader>
|
formData={formData}
|
||||||
<CardTitle className="flex items-center gap-2">
|
setFormData={setFormData}
|
||||||
<Zap className="h-5 w-5 text-yellow-500" />
|
agentId={agentId}
|
||||||
{t('oneClickAutomaticInstallation')}
|
agentToken={agentToken}
|
||||||
</CardTitle>
|
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||||
<CardDescription>
|
isSubmitting={isSubmitting}
|
||||||
{t('completeInstallationDescription')}
|
onSubmit={handleSubmit}
|
||||||
</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" />
|
|
||||||
{t('whatThisScriptDoes')}
|
|
||||||
</div>
|
|
||||||
<ul className="text-sm text-green-700 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>{t('runCommandOnServer')}</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" />
|
|
||||||
{t('downloadCompleteScript')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => copyToClipboard(installCommand.bash_script, "Installation script")}
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
|
||||||
{t('copyFullScript')}
|
|
||||||
</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" dangerouslySetInnerHTML={{ __html: t('localDevelopmentNotice') }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="details" className="space-y-4">
|
<TabsContent value="one-click" className="space-y-6">
|
||||||
<Card>
|
<RegionalOneClickTab
|
||||||
<CardHeader>
|
agentToken={agentToken}
|
||||||
<CardTitle>{t('generatedAgentConfiguration')}</CardTitle>
|
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||||
<CardDescription>
|
formData={formData}
|
||||||
{t('autoConfiguredDuringInstallation')}
|
agentId={agentId}
|
||||||
</CardDescription>
|
onDialogClose={handleDialogClose}
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-sm font-medium text-foreground">{t('agentId')}</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, t('agentId'))}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-sm font-medium text-foreground">{t('regionNameDetail')}</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">{t('serverIp')}</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">{t('apiEndpoint')}</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">{t('authenticationToken')}</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, t('authenticationToken'))}
|
|
||||||
>
|
|
||||||
<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" dangerouslySetInnerHTML={{ __html: t('configurationFileLocation') }} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="manual" className="space-y-4">
|
<TabsContent value="manual" className="space-y-6">
|
||||||
<Card>
|
<RegionalManualTab
|
||||||
<CardHeader>
|
agentToken={agentToken}
|
||||||
<CardTitle className="flex items-center gap-2">
|
currentPocketBaseUrl={currentPocketBaseUrl}
|
||||||
<Terminal className="h-5 w-5" />
|
formData={formData}
|
||||||
{t('manualInstallationSteps')}
|
agentId={agentId}
|
||||||
</CardTitle>
|
onDialogClose={handleDialogClose}
|
||||||
<CardDescription>
|
/>
|
||||||
{t('stepByStepManualInstallation')}
|
|
||||||
</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">{t('step1DownloadPackage')}</p>
|
|
||||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">{t('downloadAmd64Notice')}</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">{t('downloadArm64Notice')}</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">{t('step2InstallPackage')}</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">{t('step3ConfigureAgent')}</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">
|
|
||||||
{t('copySampleConfiguration')}
|
|
||||||
</code>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">{t('addConfigurationValues')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-l-4 border-blue-500 pl-4">
|
|
||||||
<p className="font-medium">{t('step4StartService')}</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">{t('step5VerifyInstallation')}</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>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
|
||||||
<Button variant="outline" onClick={resetDialog}>
|
|
||||||
{t('addAnotherAgent')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleComplete}>
|
|
||||||
{t('completeSetup')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Copy } from "lucide-react";
|
||||||
|
import { copyToClipboard } from "@/utils/copyUtils";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
|
interface RegionalAgentConfigFormProps {
|
||||||
|
formData: {
|
||||||
|
regionName: string;
|
||||||
|
agentIp: string;
|
||||||
|
};
|
||||||
|
setFormData: (data: { regionName: string; agentIp: string }) => void;
|
||||||
|
agentId: string;
|
||||||
|
agentToken: string;
|
||||||
|
currentPocketBaseUrl: string;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RegionalAgentConfigForm: React.FC<RegionalAgentConfigFormProps> = ({
|
||||||
|
formData,
|
||||||
|
setFormData,
|
||||||
|
agentId,
|
||||||
|
agentToken,
|
||||||
|
currentPocketBaseUrl,
|
||||||
|
isSubmitting,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="regionName">{t('regionName')}</Label>
|
||||||
|
<Input
|
||||||
|
id="regionName"
|
||||||
|
placeholder={t('regionNamePlaceholder')}
|
||||||
|
value={formData.regionName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, regionName: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="agentIp">{t('agentServerIpAddress')}</Label>
|
||||||
|
<Input
|
||||||
|
id="agentIp"
|
||||||
|
placeholder={t('agentIpPlaceholder')}
|
||||||
|
value={formData.agentIp}
|
||||||
|
onChange={(e) => setFormData({ ...formData, agentIp: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium">{t('agentId')}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={agentId}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||||
|
onClick={() => copyToClipboard(agentId)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium">{t('apiEndpoint')}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={currentPocketBaseUrl}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||||
|
onClick={() => copyToClipboard(currentPocketBaseUrl)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium">{t('authenticationToken')}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={agentToken}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
|
||||||
|
onClick={() => copyToClipboard(agentToken)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? t('creating') : t('createAgent')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Copy, Terminal } from "lucide-react";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { copyToClipboard } from "@/utils/copyUtils";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
|
interface RegionalManualTabProps {
|
||||||
|
agentToken: string;
|
||||||
|
currentPocketBaseUrl: string;
|
||||||
|
formData: {
|
||||||
|
regionName: string;
|
||||||
|
agentIp: string;
|
||||||
|
};
|
||||||
|
agentId: string;
|
||||||
|
onDialogClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RegionalManualTab: React.FC<RegionalManualTabProps> = ({
|
||||||
|
agentToken,
|
||||||
|
currentPocketBaseUrl,
|
||||||
|
formData,
|
||||||
|
agentId,
|
||||||
|
onDialogClose,
|
||||||
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
const getManualInstallSteps = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: t('step1DownloadPackage'),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
label: t('downloadAmd64Notice'),
|
||||||
|
command: 'wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('downloadArm64Notice'),
|
||||||
|
command: 'wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('step2InstallPackage'),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
command: 'sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb\nsudo apt-get install -f'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('step3ConfigureAgent'),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
command: 'sudo nano /etc/regional-check-agent/regional-check-agent.env'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('addConfigurationValues'),
|
||||||
|
command: `POCKETBASE_URL=${currentPocketBaseUrl}
|
||||||
|
AGENT_TOKEN=${agentToken}
|
||||||
|
AGENT_ID=${agentId}
|
||||||
|
REGION_NAME=${formData.regionName}
|
||||||
|
AGENT_IP=${formData.agentIp}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('step4StartService'),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
command: 'sudo systemctl enable regional-check-agent\nsudo systemctl start regional-check-agent'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('step5VerifyInstallation'),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
command: 'sudo systemctl status regional-check-agent\ncurl http://localhost:8091/health'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Terminal className="h-5 w-5" />
|
||||||
|
{t('manualInstallationSteps')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('stepByStepManualInstallation')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{getManualInstallSteps().map((step, stepIndex) => (
|
||||||
|
<div key={stepIndex} className="border-l-4 border-blue-500 pl-4 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm font-medium">
|
||||||
|
{stepIndex + 1}
|
||||||
|
</span>
|
||||||
|
<p className="font-medium">{step.title}</p>
|
||||||
|
</div>
|
||||||
|
{step.commands.map((cmd, cmdIndex) => (
|
||||||
|
<div key={cmdIndex} className="space-y-1">
|
||||||
|
{cmd.label && (
|
||||||
|
<p className="text-xs text-muted-foreground ml-8">{cmd.label}</p>
|
||||||
|
)}
|
||||||
|
<div className="ml-8 relative">
|
||||||
|
<pre className="bg-muted p-3 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
|
<code>{cmd.command}</code>
|
||||||
|
</pre>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={() => copyToClipboard(cmd.command)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-1" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
|
||||||
|
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">Prerequisites:</h4>
|
||||||
|
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-disc list-inside mb-3">
|
||||||
|
<li>Ensure you have root/sudo access on the target server</li>
|
||||||
|
<li>Make sure wget or curl is installed for downloading files</li>
|
||||||
|
<li>Internet connection required for downloading packages</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
|
||||||
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
The agent will start automatically and appear in your dashboard within a few minutes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button onClick={onDialogClose}>
|
||||||
|
{t('done')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Copy, Download, Zap, Play } from "lucide-react";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { copyToClipboard } from "@/utils/copyUtils";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface RegionalOneClickTabProps {
|
||||||
|
agentToken: string;
|
||||||
|
currentPocketBaseUrl: string;
|
||||||
|
formData: {
|
||||||
|
regionName: string;
|
||||||
|
agentIp: string;
|
||||||
|
};
|
||||||
|
agentId: string;
|
||||||
|
onDialogClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RegionalOneClickTab: React.FC<RegionalOneClickTabProps> = ({
|
||||||
|
agentToken,
|
||||||
|
currentPocketBaseUrl,
|
||||||
|
formData,
|
||||||
|
agentId,
|
||||||
|
onDialogClose,
|
||||||
|
}) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const getOneClickCommand = () => {
|
||||||
|
return `curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
|
||||||
|
--region-name="${formData.regionName}" \\
|
||||||
|
--agent-id="${agentId}" \\
|
||||||
|
--agent-ip="${formData.agentIp}" \\
|
||||||
|
--token="${agentToken}" \\
|
||||||
|
--pocketbase-url="${currentPocketBaseUrl}"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadScript = () => {
|
||||||
|
const scriptContent = getOneClickCommand();
|
||||||
|
const blob = new Blob([scriptContent], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `install-regional-agent-${agentId}.sh`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t('downloaded'),
|
||||||
|
description: t('downloadedDescription'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border-green-500/20 bg-green-50/50 dark:bg-green-950/20">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
|
||||||
|
<Zap className="h-5 w-5" />
|
||||||
|
{t('oneClickAutomaticInstallation')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-green-600 dark:text-green-300">
|
||||||
|
{t('completeInstallationDescription')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 dark:bg-green-950/50 dark:border-green-800">
|
||||||
|
<div className="flex items-center gap-2 text-green-800 dark:text-green-200 font-medium mb-2">
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
{t('whatThisScriptDoes')}
|
||||||
|
</div>
|
||||||
|
<ul className="text-sm text-green-700 dark:text-green-300 space-y-1 ml-6">
|
||||||
|
<li>• {t('scriptActionDownload')}</li>
|
||||||
|
<li>• {t('scriptActionInstall')}</li>
|
||||||
|
<li>• {t('scriptActionConfigure')}</li>
|
||||||
|
<li>• {t('scriptActionStart')}</li>
|
||||||
|
<li>• {t('scriptActionHealthChecks')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-green-700 dark:text-green-400">{t('runCommandOnServer')}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
readOnly
|
||||||
|
value={getOneClickCommand()}
|
||||||
|
className="font-mono text-sm min-h-[120px] pr-12 bg-muted/50 border-green-200 dark:border-green-800"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="absolute top-2 right-2 bg-green-50 dark:bg-green-950/50 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900"
|
||||||
|
onClick={() => copyToClipboard(getOneClickCommand())}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={downloadScript} variant="outline" className="flex-1">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
{t('downloadCompleteScript')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 dark:bg-yellow-950/20 dark:border-yellow-800">
|
||||||
|
<p className="text-sm text-yellow-800 dark:text-yellow-200" dangerouslySetInnerHTML={{ __html: t('localDevelopmentNotice') }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button onClick={onDialogClose}>
|
||||||
|
{t('done')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user