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:
lyker189
2025-10-02 05:10:11 +07:00
parent a4364936e3
commit b6ef4ff7a5
62 changed files with 2721 additions and 639 deletions
@@ -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>