feat(i18n): localize frontend components and add missing translations
- Replaced hardcoded UI strings with translation keys across frontend files - Added and updated English(en) and Khmer(km) translation files for settings, notifications, and server components - Improved localization and multi-language support throughout the frontend
This commit is contained in:
@@ -16,6 +16,7 @@ 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;
|
||||
@@ -28,6 +29,7 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
onOpenChange,
|
||||
onAgentAdded,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@@ -62,8 +64,8 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
|
||||
if (!formData.serverName || !formData.osType) {
|
||||
toast({
|
||||
title: "Validation Error",
|
||||
description: "Please fill in all required fields.",
|
||||
title: t('validationError'),
|
||||
description: t('fillRequiredFields'),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -77,8 +79,8 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
toast({
|
||||
title: "Server Agent Created",
|
||||
description: `${formData.serverName} monitoring agent has been configured successfully.`,
|
||||
title: t('serverAgentCreated'),
|
||||
description: t('serverAgentCreatedDesc').replace('{name}', formData.serverName),
|
||||
});
|
||||
|
||||
// Switch to one-click install tab after successful creation
|
||||
@@ -86,8 +88,8 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
onAgentAdded();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create server monitoring agent.",
|
||||
title: t('error'),
|
||||
description: t('failedToCreateAgent'),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -116,19 +118,19 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5" />
|
||||
Add Server Monitoring Agent
|
||||
{t('addServerMonitoringAgent')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure a new server monitoring agent to track system metrics and performance.
|
||||
{t('configureAgentDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="configure">Configure Agent</TabsTrigger>
|
||||
<TabsTrigger value="one-click">One-Click Install</TabsTrigger>
|
||||
<TabsTrigger value="docker-one-click">Docker One-Click</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual Installation</TabsTrigger>
|
||||
<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>
|
||||
|
||||
<TabsContent value="configure" className="space-y-6">
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -26,6 +27,7 @@ export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
|
||||
serverId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const getDockerOneClickCommand = () => {
|
||||
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
|
||||
|
||||
@@ -86,15 +88,15 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
|
||||
<Container className="h-5 w-5" />
|
||||
Docker One-Click Install
|
||||
{t('dockerOneClickTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-blue-600 dark:text-blue-300">
|
||||
Automated Docker container installation with system monitoring capabilities
|
||||
{t('dockerOneClickDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-blue-700 dark:text-blue-400">Docker One-Click Command</Label>
|
||||
<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>
|
||||
@@ -107,18 +109,18 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
onClick={handleCopyOneClickCommand}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{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">This script will automatically:</p>
|
||||
<p className="font-medium mb-1">{t('dockerScriptWill')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Download and setup the Docker monitoring agent</li>
|
||||
<li>Configure all required environment variables</li>
|
||||
<li>Start the container with proper system access</li>
|
||||
<li>Setup monitoring data persistence</li>
|
||||
<li>{t('dockerScriptStep1')}</li>
|
||||
<li>{t('dockerScriptStep2')}</li>
|
||||
<li>{t('dockerScriptStep3')}</li>
|
||||
<li>{t('dockerScriptStep4')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -129,15 +131,15 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Container className="h-5 w-5" />
|
||||
Direct Docker Run Command
|
||||
{t('directDockerTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
If you prefer to run the Docker container directly without the script
|
||||
{t('directDockerDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Docker Run Command</Label>
|
||||
<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>
|
||||
@@ -150,17 +152,17 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
onClick={handleCopyDockerCommand}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{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">Prerequisites for direct Docker run:</p>
|
||||
<p className="font-medium mb-1">{t('dockerPrerequisites')}</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Docker must be installed and running</li>
|
||||
<li>The operacle/checkcle-server-agent image must be available</li>
|
||||
<li>Run as root or with sudo privileges</li>
|
||||
<li>{t('dockerPrereqStep1')}</li>
|
||||
<li>{t('dockerPrereqStep2')}</li>
|
||||
<li>{t('dockerPrereqStep3')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -168,7 +170,7 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
Done
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RefreshCw, X } from "lucide-react";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { templateService, ServerNotificationTemplate } from "@/services/templateService";
|
||||
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface EditServerDialogProps {
|
||||
server: Server | null;
|
||||
@@ -46,6 +47,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
onOpenChange,
|
||||
onServerUpdated,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [formData, setFormData] = useState<ServerFormData>({
|
||||
name: "",
|
||||
check_interval: 60,
|
||||
@@ -114,7 +116,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// 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 as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((existingThreshold.ram_threshold_message ?? existingThreshold.ram_threshold))) || 80,
|
||||
disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80,
|
||||
network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80,
|
||||
});
|
||||
@@ -141,7 +143,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 as any).ram_threshold_message || threshold.ram_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((threshold.ram_threshold_message ?? threshold.ram_threshold))) || 80,
|
||||
disk_threshold: parseInt(String(threshold.disk_threshold)) || 80,
|
||||
network_threshold: parseInt(String(threshold.network_threshold)) || 80,
|
||||
});
|
||||
@@ -165,8 +167,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load notification channels",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToLoadChannels', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setLoadingAlertConfigs(false);
|
||||
@@ -182,8 +184,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load templates",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToLoadTemplates', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setLoadingTemplates(false);
|
||||
@@ -198,8 +200,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load server thresholds",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToLoadThresholds', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setLoadingThresholds(false);
|
||||
@@ -274,15 +276,15 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Warning",
|
||||
description: "Server updated but failed to update threshold values.",
|
||||
title: t('warning', 'instance'),
|
||||
description: t('serverUpdatedButThresholdFailed', 'instance'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Server updated",
|
||||
description: `${formData.name} has been updated successfully.`,
|
||||
title: t('serverUpdated', 'instance'),
|
||||
description: t('serverUpdatedDesc', 'instance', { name: formData.name }),
|
||||
});
|
||||
|
||||
onServerUpdated();
|
||||
@@ -291,8 +293,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to update server. Please try again.",
|
||||
title: t('error', 'instance'),
|
||||
description: t('failedToUpdateServer', 'instance'),
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -323,62 +325,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>Edit Server Configuration</DialogTitle>
|
||||
<DialogTitle>{t('editServerConfiguration', 'instance')}</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">Server Name *</Label>
|
||||
<Label htmlFor="serverName">{t('serverNameLabel', 'instance')}</Label>
|
||||
<Input
|
||||
id="serverName"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Enter server name"
|
||||
placeholder={t('serverNamePlaceholder', 'instance')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="checkInterval">Check Interval</Label>
|
||||
<Label htmlFor="checkInterval">{t('checkIntervalLabel', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.check_interval.toString()}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, check_interval: parseInt(value) }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select interval" />
|
||||
<SelectValue placeholder={t('selectInterval', 'instance')} />
|
||||
</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>
|
||||
<SelectItem value="600">10 minutes</SelectItem>
|
||||
<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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxRetries">Max Retries</Label>
|
||||
<Label htmlFor="maxRetries">{t('maxRetriesLabel', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.max_retries.toString()}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, max_retries: parseInt(value) }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select max retries" />
|
||||
<SelectValue placeholder={t('selectMaxRetries', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 retry</SelectItem>
|
||||
<SelectItem value="2">2 retries</SelectItem>
|
||||
<SelectItem value="3">3 retries</SelectItem>
|
||||
<SelectItem value="5">5 retries</SelectItem>
|
||||
<SelectItem value="10">10 retries</SelectItem>
|
||||
<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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dockerMonitoring">Docker Monitoring</Label>
|
||||
<Label htmlFor="dockerMonitoring">{t('dockerMonitoring', 'instance')}</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="dockerMonitoring"
|
||||
@@ -389,7 +391,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="dockerMonitoring" className="text-sm text-muted-foreground">
|
||||
{formData.docker_monitoring ? "Enabled" : "Disabled"}
|
||||
{formData.docker_monitoring ? t('enabled', 'instance') : t('disabled', 'instance')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -407,22 +409,22 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Remove the automatic clearing of notification_channels, threshold_id, and template_id
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
|
||||
<Label htmlFor="notificationEnabled">{t('enableNotifications', 'instance')}</Label>
|
||||
</div>
|
||||
|
||||
{/* Expanded Notification Settings */}
|
||||
{formData.notification_enabled && (
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Notification Settings</CardTitle>
|
||||
<CardTitle className="text-lg">{t('notificationSettings', 'instance')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Multiple Notification Channels Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Notification Channels</Label>
|
||||
<Label>{t('notificationChannels', 'instance')}</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">Loading channels...</div>
|
||||
<div className="text-sm text-muted-foreground">{t('loadingChannels', 'instance')}</div>
|
||||
) : alertConfigs.length > 0 ? (
|
||||
alertConfigs.map((config) => (
|
||||
<div key={config.id} className="flex items-center space-x-2">
|
||||
@@ -442,14 +444,14 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">No notification channels available</div>
|
||||
<div className="text-sm text-muted-foreground">{t('noChannelsAvailable', 'instance')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Channels Display */}
|
||||
{formData.notification_channels.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Selected Channels:</Label>
|
||||
<Label className="text-sm font-medium">{t('selectedChannels', 'instance')}</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.notification_channels.map((channelId) => {
|
||||
const channel = alertConfigs.find(c => c.id === channelId);
|
||||
@@ -478,17 +480,17 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
|
||||
{/* Server Set Threshold Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thresholdId">Server Set Threshold</Label>
|
||||
<Label htmlFor="thresholdId">{t('serverSetThreshold', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.threshold_id}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, threshold_id: value }))}
|
||||
disabled={loadingThresholds}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingThresholds ? "Loading thresholds..." : "Select server threshold"} />
|
||||
<SelectValue placeholder={loadingThresholds ? t('loadingThresholds', 'instance') : t('selectServerThreshold', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No threshold (use default)</SelectItem>
|
||||
<SelectItem value="none">{t('noThreshold', 'instance')}</SelectItem>
|
||||
{thresholds.map((threshold) => (
|
||||
<SelectItem key={threshold.id} value={threshold.id}>
|
||||
{threshold.name}
|
||||
@@ -502,12 +504,12 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
{selectedThreshold && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
|
||||
<CardTitle className="text-base">{t('thresholdDetails', 'instance')}: {selectedThreshold.name}</CardTitle>
|
||||
</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">CPU Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('cpuThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -521,7 +523,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('ramThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -535,7 +537,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('diskThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -549,7 +551,7 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Network Threshold (%)</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('networkThresholdPct', 'instance')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -569,17 +571,17 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
|
||||
{/* Server Template Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="templateId">Server Template</Label>
|
||||
<Label htmlFor="templateId">{t('serverTemplate', 'instance')}</Label>
|
||||
<Select
|
||||
value={formData.template_id}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, template_id: value }))}
|
||||
disabled={loadingTemplates}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingTemplates ? "Loading templates..." : "Select server template"} />
|
||||
<SelectValue placeholder={loadingTemplates ? t('loadingTemplates', 'instance') : t('selectServerTemplate', 'instance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No template (use default)</SelectItem>
|
||||
<SelectItem value="none">{t('noTemplate', 'instance')}</SelectItem>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
@@ -593,44 +595,44 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
{selectedTemplate && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Template Details: {selectedTemplate.name}</CardTitle>
|
||||
<CardTitle className="text-base">{t('templateDetails', 'instance')}: {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">RAM Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('ramMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.ram_message || "No RAM message defined"}
|
||||
{selectedTemplate.ram_message || t('noMessageDefined', 'instance', { name: 'RAM' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">CPU Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('cpuMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.cpu_message || "No CPU message defined"}
|
||||
{selectedTemplate.cpu_message || t('noMessageDefined', 'instance', { name: 'CPU' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Disk Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('diskMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.disk_message || "No disk message defined"}
|
||||
{selectedTemplate.disk_message || t('noMessageDefined', 'instance', { name: 'disk' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Network Message</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">{t('networkMessage', 'instance')}</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.network_message || "No network message defined"}
|
||||
{selectedTemplate.network_message || t('noMessageDefined', 'instance', { name: 'network' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Up Message</Label>
|
||||
<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 || "No up message defined"}
|
||||
{selectedTemplate.up_message || t('noMessageDefined', 'instance', { name: 'up' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Down Message</Label>
|
||||
<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 || "No down message defined"}
|
||||
{selectedTemplate.down_message || t('noMessageDefined', 'instance', { name: 'down' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -649,16 +651,16 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel', 'instance')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Updating...
|
||||
{t('updating', 'instance')}
|
||||
</>
|
||||
) : (
|
||||
"Update Server"
|
||||
t('updateServer', 'instance')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -24,20 +25,22 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
serverId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const getManualInstallSteps = () => {
|
||||
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
|
||||
|
||||
return [
|
||||
{
|
||||
title: "Download the installation script",
|
||||
title: t('downloadScript'),
|
||||
command: `curl -L -o server-agent.sh "${scriptUrl}"`
|
||||
},
|
||||
{
|
||||
title: "Make the script executable",
|
||||
title: t('makeExecutable'),
|
||||
command: `chmod +x server-agent.sh`
|
||||
},
|
||||
{
|
||||
title: "Run the installation with your configuration",
|
||||
title: t('runInstall'),
|
||||
command: `SERVER_TOKEN="${serverToken}" POCKETBASE_URL="${currentPocketBaseUrl}" SERVER_NAME="${formData.serverName}" AGENT_ID="${serverId}" sudo bash server-agent.sh`
|
||||
}
|
||||
];
|
||||
@@ -48,26 +51,26 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5" />
|
||||
Manual Installation Steps
|
||||
{t('manualInstallTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Step-by-step installation process
|
||||
{t('manualInstallDesc')}
|
||||
</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}
|
||||
<span className="font-medium">{t('serverName')}:</span> {formData.serverName}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Agent ID:</span> {serverId}
|
||||
<span className="font-medium">{t('agentId')}:</span> {serverId}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">OS Type:</span> {formData.osType}
|
||||
<span className="font-medium">{t('osType')}:</span> {formData.osType}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Check Interval:</span> {formData.checkInterval}s
|
||||
<span className="font-medium">{t('checkInterval')}:</span> {formData.checkInterval}s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +96,7 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
|
||||
onClick={() => copyToClipboard(step.command)}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,22 +104,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">Prerequisites:</h4>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">{t('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>
|
||||
<li>{t('prereqRoot')}</li>
|
||||
<li>{t('prereqCurl')}</li>
|
||||
<li>{t('prereqInternet')}</li>
|
||||
</ul>
|
||||
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">After Installation:</h4>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">{t('afterInstall')}</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.
|
||||
{t('agentWillStart')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
Done
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -26,6 +27,8 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
|
||||
serverId,
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const getOneClickInstallCommand = () => {
|
||||
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
|
||||
|
||||
@@ -55,15 +58,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" />
|
||||
One-Click Install (Recommended)
|
||||
{t('oneClickInstallTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-green-600 dark:text-green-300">
|
||||
Copy and paste this single command to install the monitoring agent instantly
|
||||
{t('oneClickInstallDesc')}
|
||||
</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>
|
||||
<Label className="text-green-700 dark:text-green-400">{t('quickInstallCommand')}</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>
|
||||
@@ -76,23 +79,23 @@ sudo -E bash ./server-agent.sh`;
|
||||
onClick={handleCopyCommand}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
{t('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>
|
||||
<p className="font-medium mb-1">{t('runCommandOnServer')}</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>
|
||||
<li>{t('sshIntoServer')}</li>
|
||||
<li>{t('pasteAndRun')}</li>
|
||||
<li>{t('agentInstalled')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={onDialogClose}>
|
||||
Done
|
||||
{t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -7,6 +7,7 @@ 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: {
|
||||
@@ -43,23 +44,25 @@ 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">Server Name *</Label>
|
||||
<Label htmlFor="serverName">{t('serverName')} *</Label>
|
||||
<Input
|
||||
id="serverName"
|
||||
placeholder="e.g., web-server-01"
|
||||
placeholder={t('serverNamePlaceholder')}
|
||||
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>
|
||||
<p className="text-xs text-muted-foreground">{t('serverNameDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="serverId">Server Agent ID</Label>
|
||||
<Label htmlFor="serverId">{t('serverAgentId')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="serverId"
|
||||
@@ -76,13 +79,13 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Auto-generated unique identifier</p>
|
||||
<p className="text-xs text-muted-foreground">{t('serverAgentIdDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Operating System *</Label>
|
||||
<Label>{t('operatingSystem')} *</Label>
|
||||
<OSSelector
|
||||
value={formData.osType}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, osType: value }))}
|
||||
@@ -91,45 +94,45 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="checkInterval">Check Interval</Label>
|
||||
<Label htmlFor="checkInterval">{t('checkInterval')}</Label>
|
||||
<Select
|
||||
value={formData.checkInterval}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, checkInterval: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select interval" />
|
||||
<SelectValue placeholder={t('selectInterval')} />
|
||||
</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>
|
||||
<SelectItem value="30">{t('interval30s')}</SelectItem>
|
||||
<SelectItem value="60">{t('interval1m')}</SelectItem>
|
||||
<SelectItem value="120">{t('interval2m')}</SelectItem>
|
||||
<SelectItem value="300">{t('interval5m')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">How often to check the server and metric status</p>
|
||||
<p className="text-xs text-muted-foreground">{t('checkIntervalDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="retryAttempt">Retry Attempts</Label>
|
||||
<Label htmlFor="retryAttempt">{t('retryAttempts')}</Label>
|
||||
<Select
|
||||
value={formData.retryAttempt}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, retryAttempt: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select retry attempts" />
|
||||
<SelectValue placeholder={t('selectRetryAttempts')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 attempt</SelectItem>
|
||||
<SelectItem value="2">2 attempts</SelectItem>
|
||||
<SelectItem value="3">3 attempts</SelectItem>
|
||||
<SelectItem value="5">5 attempts</SelectItem>
|
||||
<SelectItem value="1">{t('attempt1')}</SelectItem>
|
||||
<SelectItem value="2">{t('attempt2')}</SelectItem>
|
||||
<SelectItem value="3">{t('attempt3')}</SelectItem>
|
||||
<SelectItem value="5">{t('attempt5')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Number of retry attempts before marking as down</p>
|
||||
<p className="text-xs text-muted-foreground">{t('retryAttemptsDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Server Token</Label>
|
||||
<Label>{t('serverToken')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={serverToken} readOnly className="font-mono text-sm bg-muted" />
|
||||
<Button
|
||||
@@ -141,11 +144,11 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Auto-generated authentication token</p>
|
||||
<p className="text-xs text-muted-foreground">{t('serverTokenDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>System URL</Label>
|
||||
<Label>{t('systemUrl')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input value={currentPocketBaseUrl} readOnly className="font-mono text-sm bg-muted" />
|
||||
<Button
|
||||
@@ -157,14 +160,14 @@ export const ServerAgentConfigForm: React.FC<ServerAgentConfigFormProps> = ({
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Current system API URL</p>
|
||||
<p className="text-xs text-muted-foreground">{t('systemUrlDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button type="submit" disabled={isSubmitting} className="w-full">
|
||||
{isSubmitting ? "Creating Agent..." : "Create Server Agent"}
|
||||
{isSubmitting ? t('creatingAgent') : t('createServerAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -8,12 +8,14 @@ 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,
|
||||
@@ -33,15 +35,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">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</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">Loading...</span>
|
||||
<span className="text-xs text-muted-foreground">{t("loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
@@ -77,7 +79,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">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
|
||||
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
@@ -85,9 +87,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">Error loading chart data</p>
|
||||
<p className="text-muted-foreground">{t("errorLoadingChartData")}</p>
|
||||
<p className="text-xs mt-2 font-mono text-red-500">{error?.message}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">Server ID: {serverId} • Time Range: {timeRange}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">{t("serverIdTimeRange", { serverId, timeRange })}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -101,7 +103,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">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</h2>
|
||||
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
@@ -109,11 +111,13 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<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-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-xs mt-1 text-muted-foreground">
|
||||
{metrics.length > 0 ? 'Data exists but may be outside selected time range' : 'No metrics data found'}
|
||||
{metrics.length > 0
|
||||
? t("dataExistsOutsideRange")
|
||||
: t("noMetricsDataFound")}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -129,13 +133,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">Historical Performance</h2>
|
||||
<h2 className="text-lg font-medium">{t("historicalPerformance")}</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">Updating...</span>
|
||||
<span className="text-blue-500">{t("updating")}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -3,17 +3,19 @@ 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: "TOTAL SERVERS",
|
||||
title: t('totalServers', 'instance'),
|
||||
value: stats.total,
|
||||
icon: Server,
|
||||
gradient: theme === 'dark'
|
||||
@@ -21,7 +23,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #a0522d 100%)"
|
||||
},
|
||||
{
|
||||
title: "ONLINE SERVERS",
|
||||
title: t('onlineServers', 'instance'),
|
||||
value: stats.online,
|
||||
icon: Activity,
|
||||
gradient: theme === 'dark'
|
||||
@@ -29,7 +31,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #66bb6a 100%)"
|
||||
},
|
||||
{
|
||||
title: "OFFLINE SERVERS",
|
||||
title: t('offlineServers', 'instance'),
|
||||
value: stats.offline,
|
||||
icon: Power,
|
||||
gradient: theme === 'dark'
|
||||
@@ -37,7 +39,7 @@ export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #ef5350 100%)"
|
||||
},
|
||||
{
|
||||
title: "WARNING SERVERS",
|
||||
title: t('warningServers', 'instance'),
|
||||
value: stats.warning,
|
||||
icon: AlertTriangle,
|
||||
gradient: theme === 'dark'
|
||||
|
||||
@@ -16,6 +16,7 @@ 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[];
|
||||
@@ -24,6 +25,7 @@ interface ServerTableProps {
|
||||
}
|
||||
|
||||
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
|
||||
const { t } = useLanguage();
|
||||
const { theme } = useTheme();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -68,8 +70,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
await pb.collection('servers').update(serverId, updateData);
|
||||
|
||||
toast({
|
||||
title: isPaused ? "Server resumed" : "Server paused",
|
||||
description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
|
||||
title: isPaused ? t('serverResumed') : t('serverPaused'),
|
||||
description: isPaused ? t('monitoringResumed', { name: server.name }) : t('monitoringPaused', { name: server.name }),
|
||||
});
|
||||
|
||||
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
|
||||
@@ -81,8 +83,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
// console.error('Error updating server status:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
|
||||
title: t('error'),
|
||||
description: isPaused ? t('resumeServerError') : t('pauseServerError'),
|
||||
});
|
||||
} finally {
|
||||
setPausingServers(prev => {
|
||||
@@ -204,12 +206,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
return (
|
||||
<Card className="flex-1 flex flex-col">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<CardTitle>Servers</CardTitle>
|
||||
<CardTitle>{t('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">Loading servers...</span>
|
||||
<span className="ml-2">{t('loadingServers')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -221,12 +223,12 @@ 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">Servers</CardTitle>
|
||||
<CardTitle className="text-xl font-semibold">{t('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="Search servers..."
|
||||
placeholder={t('searchServersPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
@@ -241,23 +243,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">No servers found</p>
|
||||
<p className="text-muted-foreground">{t('noServersFound')}</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'} 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>
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -333,7 +335,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<span className="sr-only">{t('openMenu')}</span>
|
||||
{isProcessing ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
@@ -344,12 +346,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Server Detail
|
||||
{t('viewServerDetail')}
|
||||
</DropdownMenuItem>
|
||||
{server.docker === 'true' && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Container Monitoring
|
||||
{t('containerMonitoring')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
@@ -360,26 +362,26 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
{isPaused ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Resume Monitoring
|
||||
{t('resumeMonitoring')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
Pause Monitoring
|
||||
{t('pauseMonitoring')}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Server
|
||||
{t('editServer')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Server
|
||||
{t('deleteServer')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -406,25 +408,21 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteServerConfirmTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete{' '}
|
||||
<span className="font-semibold text-foreground">
|
||||
{selectedServer?.name}
|
||||
</span>{' '}
|
||||
and all of its monitoring data.
|
||||
{t('deleteServerConfirmDesc').replace('{name}', selectedServer?.name ?? '')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 text-white hover:bg-red-700"
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
{isDeleting ? t('deleting') : t('delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user