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
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody } from "@/components/ui/table";
import { DockerContainer } from "@/types/docker.types";
import { DockerMetricsDialog } from "./DockerMetricsDialog";
import { useLanguage } from "@/contexts/LanguageContext";
import {
DockerTableSearch,
DockerTableHeader,
@@ -18,6 +19,7 @@ interface DockerContainersTableProps {
}
export const DockerContainersTable = ({ containers, isLoading, onRefresh }: DockerContainersTableProps) => {
const { t } = useLanguage();
const [searchTerm, setSearchTerm] = useState("");
const [selectedContainer, setSelectedContainer] = useState<DockerContainer | null>(null);
const [metricsDialogOpen, setMetricsDialogOpen] = useState(false);
@@ -49,7 +51,7 @@ export const DockerContainersTable = ({ containers, isLoading, onRefresh }: Dock
<CardHeader className="pb-4 px-0">
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="text-lg sm:text-xl font-semibold">Docker Containers</CardTitle>
<CardTitle className="text-lg sm:text-xl font-semibold">{t('dockerContainers', 'docker')}</CardTitle>
<DockerTableSearch
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
@@ -10,6 +10,7 @@ import { DockerContainer, DockerMetrics } from "@/types/docker.types";
import { dockerService } from "@/services/dockerService";
import { Loader2, Cpu, HardDrive, Network, MemoryStick } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerMetricsDialogProps {
container: DockerContainer | null;
@@ -20,16 +21,17 @@ interface DockerMetricsDialogProps {
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
const timeRangeOptions = [
{ value: '60m' as TimeRange, label: '60 minutes', hours: 1 },
{ value: '1d' as TimeRange, label: '1 day', hours: 24 },
{ value: '7d' as TimeRange, label: '7 days', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: '1 month', hours: 24 * 30 },
{ value: '3m' as TimeRange, label: '3 months', hours: 24 * 90 },
{ value: '60m' as TimeRange, label: 'minutes60', hours: 1 },
{ value: '1d' as TimeRange, label: 'day1', hours: 24 },
{ value: '7d' as TimeRange, label: 'days7', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: 'month1', hours: 24 * 30 },
{ value: '3m' as TimeRange, label: 'months3', hours: 24 * 90 },
];
export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMetricsDialogProps) => {
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
const { theme } = useTheme();
const { t } = useLanguage();
const {
data: metrics = [],
@@ -232,11 +234,11 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<DialogContent className="max-w-7xl max-h-[95vh] overflow-y-auto bg-background text-foreground">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
<Cpu className="h-4 w-4 text-primary" />
</div>
Container Metrics: {container.name}
{t('containerMetricsTitle', 'docker', { name: container.name })}
</div>
<div className="flex items-center gap-2">
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
@@ -246,7 +248,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<SelectContent>
{timeRangeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(option.label, 'docker')}
</SelectItem>
))}
</SelectContent>
@@ -254,22 +256,22 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
</div>
</DialogTitle>
<p className="text-sm text-muted-foreground">
Docker ID: {container.docker_id} {container.hostname}
{t('dockerId', 'docker')}: {container.docker_id} {container.hostname}
</p>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center h-96">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Loading metrics...</span>
<span className="ml-2">{t('loadingMetrics', 'docker')}</span>
</div>
) : error ? (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>Error loading metrics: {error.message}</p>
<p>{t('errorLoadingMetrics', 'docker')}: {String((error as any)?.message ?? '')}</p>
</div>
) : chartData.length === 0 ? (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>No metrics data available for this container</p>
<p>{t('noMetricsAvailable', 'docker')}</p>
</div>
) : (
<>
@@ -277,7 +279,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
{latestMetric && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<MetricCard
title="CPU"
title={t('cpu', 'docker')}
used={`${latestMetric.cpuUsage}%`}
total={`${latestMetric.cpuCores} cores`}
free={`${(100 - latestMetric.cpuUsage).toFixed(1)}%`}
@@ -286,7 +288,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
color={chartConfig.cpuUsage.color}
/>
<MetricCard
title="Memory"
title={t('memoryTitle', 'docker')}
used={latestMetric.ramUsed}
total={latestMetric.ramTotal}
free={latestMetric.ramFree}
@@ -295,7 +297,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
color={chartConfig.ramUsagePercent.color}
/>
<MetricCard
title="Disk"
title={t('diskTitle', 'docker')}
used={latestMetric.diskUsed}
total={latestMetric.diskTotal}
free={latestMetric.diskFree}
@@ -304,10 +306,10 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
color={chartConfig.diskUsagePercent.color}
/>
<MetricCard
title="Network"
title={t('network', 'docker')}
used={`RX: ${latestMetric.networkRx}`}
total={`TX: ${latestMetric.networkTx}`}
free={`Speed: ${latestMetric.networkRxSpeed} KB/s`}
free={`${t('networkSpeedKbs', 'docker').replace('(KB/s)', '')}: ${latestMetric.networkRxSpeed} KB/s`}
percentage={0}
icon={Network}
color={chartConfig.networkRx.color}
@@ -319,19 +321,19 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<TabsList className="grid w-full grid-cols-4 bg-muted">
<TabsTrigger value="cpu" className="flex items-center gap-2 data-[state=active]:bg-background">
<Cpu className="h-4 w-4" />
CPU
{t('cpu', 'docker')}
</TabsTrigger>
<TabsTrigger value="memory" className="flex items-center gap-2 data-[state=active]:bg-background">
<MemoryStick className="h-4 w-4" />
Memory
{t('memoryTitle', 'docker')}
</TabsTrigger>
<TabsTrigger value="disk" className="flex items-center gap-2 data-[state=active]:bg-background">
<HardDrive className="h-4 w-4" />
Disk
{t('diskTitle', 'docker')}
</TabsTrigger>
<TabsTrigger value="network" className="flex items-center gap-2 data-[state=active]:bg-background">
<Network className="h-4 w-4" />
Network
{t('network', 'docker')}
</TabsTrigger>
</TabsList>
@@ -339,7 +341,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">CPU Usage (%)</CardTitle>
<CardTitle className="text-foreground">{t('cpuUsagePct', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -365,7 +367,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.cpuUsage.color}
strokeWidth={2}
dot={{ r: 3, fill: chartConfig.cpuUsage.color }}
name="CPU Usage (%)"
name={t('cpuUsagePct', 'docker')}
/>
</LineChart>
</ChartContainer>
@@ -374,7 +376,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">CPU Usage vs Available</CardTitle>
<CardTitle className="text-foreground">{t('cpuUsageVsAvailable', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -401,7 +403,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name="CPU Available (%)"
name={t('cpuAvailablePct', 'docker')}
/>
<Area
type="monotone"
@@ -410,7 +412,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.cpuUsage.color}
fill={chartConfig.cpuUsage.color}
fillOpacity={0.6}
name="CPU Usage (%)"
name={t('cpuUsagePct', 'docker')}
/>
</AreaChart>
</ChartContainer>
@@ -423,7 +425,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Memory Usage (%)</CardTitle>
<CardTitle className="text-foreground">{t('ramUsagePct', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -459,7 +461,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Memory Usage (Bytes)</CardTitle>
<CardTitle className="text-foreground">{t('memoryUsageBytes', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -479,8 +481,8 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
formatter={(value, name) => [
name === 'Used Memory' ? formatBytes(Number(value)) :
name === 'Total Memory' ? formatBytes(Number(value)) : value,
name === t('usedMemory', 'docker') ? formatBytes(Number(value)) :
name === t('totalMemory', 'docker') ? formatBytes(Number(value)) : value,
name
]}
/>
@@ -491,7 +493,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name="Total Memory"
name={t('totalMemory', 'docker')}
/>
<Area
type="monotone"
@@ -500,7 +502,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.ramUsagePercent.color}
fill={chartConfig.ramUsagePercent.color}
fillOpacity={0.6}
name="Used Memory"
name={t('usedMemory', 'docker')}
/>
</AreaChart>
</ChartContainer>
@@ -513,7 +515,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Disk Usage (%)</CardTitle>
<CardTitle className="text-foreground">{t('diskUsagePct', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -540,7 +542,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.diskUsagePercent.color}
fill={chartConfig.diskUsagePercent.color}
fillOpacity={0.6}
name="Disk Usage (%)"
name={t('diskUsagePct', 'docker')}
/>
</AreaChart>
</ChartContainer>
@@ -549,7 +551,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Disk Usage (Bytes)</CardTitle>
<CardTitle className="text-foreground">{t('diskUsageBytes', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-80">
@@ -569,8 +571,8 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
content={<ChartTooltipContent className="bg-popover border-border" />}
cursor={{ stroke: getGridColor() }}
formatter={(value, name) => [
name === 'Used Disk' ? formatBytes(Number(value)) :
name === 'Total Disk' ? formatBytes(Number(value)) : value,
name === t('usedDisk', 'docker') ? formatBytes(Number(value)) :
name === t('totalDisk', 'docker') ? formatBytes(Number(value)) : value,
name
]}
/>
@@ -581,7 +583,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
fillOpacity={0.3}
name="Total Disk"
name={t('totalDisk', 'docker')}
/>
<Area
type="monotone"
@@ -590,7 +592,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
stroke={chartConfig.diskUsagePercent.color}
fill={chartConfig.diskUsagePercent.color}
fillOpacity={0.6}
name="Used Disk"
name={t('usedDisk', 'docker')}
/>
</AreaChart>
</ChartContainer>
@@ -603,7 +605,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Network Traffic</CardTitle>
<CardTitle className="text-foreground">{t('networkTraffic', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-64">
@@ -627,7 +629,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkRxBytes"
stroke={chartConfig.networkRx.color}
strokeWidth={2}
name="RX Bytes"
name={t('rxBytes', 'docker')}
dot={{ r: 2 }}
/>
<Line
@@ -635,7 +637,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkTxBytes"
stroke={chartConfig.networkTx.color}
strokeWidth={2}
name="TX Bytes"
name={t('txBytes', 'docker')}
dot={{ r: 2 }}
/>
</LineChart>
@@ -645,7 +647,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Network Speed (KB/s)</CardTitle>
<CardTitle className="text-foreground">{t('networkSpeedKbs', 'docker')}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-64">
@@ -669,7 +671,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkRxSpeed"
stroke={chartConfig.networkRx.color}
strokeWidth={2}
name="RX Speed (KB/s)"
name={t('rxSpeedKbs', 'docker')}
dot={{ r: 2 }}
/>
<Line
@@ -677,7 +679,7 @@ export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMet
dataKey="networkTxSpeed"
stroke={chartConfig.networkTx.color}
strokeWidth={2}
name="TX Speed (KB/s)"
name={t('txSpeedKbs', 'docker')}
dot={{ r: 2 }}
/>
</LineChart>
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
import { Container, Play, Square, AlertTriangle } from "lucide-react";
import { DockerStats } from "@/types/docker.types";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerStatsCardsProps {
stats: DockerStats;
@@ -11,10 +12,11 @@ interface DockerStatsCardsProps {
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
const { theme } = useTheme();
const { t } = useLanguage();
const cards = [
{
title: "Total Containers",
title: t('totalContainers', 'docker'),
value: stats.total,
icon: Container,
color: "text-blue-600",
@@ -23,7 +25,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)"
},
{
title: "Running",
title: t('running', 'docker'),
value: stats.running,
icon: Play,
color: "text-green-600",
@@ -32,7 +34,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)"
},
{
title: "Stopped",
title: t('stopped', 'docker'),
value: stats.stopped,
icon: Square,
color: "text-gray-600",
@@ -41,7 +43,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)"
},
{
title: "Warning",
title: t('warning', 'docker'),
value: stats.warning,
icon: AlertTriangle,
color: "text-amber-600",
@@ -90,7 +92,7 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
variant="outline"
className="text-xs font-mono font-bold px-2 py-1 rounded-md bg-white/20 backdrop-blur-sm text-white border border-white/30"
>
Containers
{t('containersLabel', 'docker')}
</Badge>
</div>
</CardContent>
@@ -1,36 +1,38 @@
import { Badge } from "@/components/ui/badge";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerStatusBadgeProps {
status: 'running' | 'stopped' | 'warning';
}
export const DockerStatusBadge = ({ status }: DockerStatusBadgeProps) => {
const { t } = useLanguage();
const getStatusConfig = (status: string) => {
switch (status) {
case 'running':
return {
variant: 'default' as const,
className: 'bg-emerald-100 text-emerald-800 border-emerald-200 hover:bg-emerald-200',
label: 'Running'
label: t('running', 'docker')
};
case 'stopped':
return {
variant: 'secondary' as const,
className: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200',
label: 'Stopped'
label: t('stopped', 'docker')
};
case 'warning':
return {
variant: 'destructive' as const,
className: 'bg-amber-100 text-amber-800 border-amber-200 hover:bg-amber-200',
label: 'Warning'
label: t('warning', 'docker')
};
default:
return {
variant: 'outline' as const,
className: 'bg-gray-100 text-gray-600 border-gray-200',
label: 'Unknown'
label: t('unknown', 'docker')
};
}
};
@@ -1,20 +1,22 @@
import { TableCell, TableRow } from "@/components/ui/table";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerEmptyStateProps {
searchTerm: string;
}
export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => {
const { t } = useLanguage();
return (
<TableRow>
<TableCell colSpan={8} className="text-center py-12 text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<div className="text-lg font-medium">
{searchTerm ? "No containers found" : "No containers running"}
{searchTerm ? t('noContainersFound', 'docker') : t('noContainersRunning', 'docker')}
</div>
<div className="text-sm">
{searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."}
{searchTerm ? t('tryAdjustSearch', 'docker') : t('startSomeContainers', 'docker')}
</div>
</div>
</TableCell>
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Eye, Play, Pause, Square, Trash2, BarChart3, RefreshCw } from "lucide-react";
import { DockerContainer } from "@/types/docker.types";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerRowActionsProps {
container: DockerContainer;
@@ -12,11 +13,12 @@ interface DockerRowActionsProps {
}
export const DockerRowActions = ({ container, containerStatus, onContainerAction, onViewMetrics }: DockerRowActionsProps) => {
const { t } = useLanguage();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-muted">
<span className="sr-only">Open menu</span>
<span className="sr-only">{t('openMenu', 'docker')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -26,14 +28,14 @@ export const DockerRowActions = ({ container, containerStatus, onContainerAction
className="cursor-pointer hover:bg-muted"
>
<BarChart3 className="mr-2 h-4 w-4" />
View Metrics
{t('viewMetrics', 'docker')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onContainerAction('view-detail', container.id, container.name)}
className="cursor-pointer hover:bg-muted"
>
<Eye className="mr-2 h-4 w-4" />
View Details
{t('viewDetails', 'docker')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -1,18 +1,20 @@
import { TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useLanguage } from "@/contexts/LanguageContext";
export const DockerTableHeader = () => {
const { t } = useLanguage();
return (
<TableHeader>
<TableRow className="border-border bg-muted/30">
<TableHead className="min-w-[200px] font-semibold">Container</TableHead>
<TableHead className="min-w-[100px] font-semibold">Status</TableHead>
<TableHead className="min-w-[140px] font-semibold">CPU Usage</TableHead>
<TableHead className="min-w-[160px] font-semibold">Memory</TableHead>
<TableHead className="min-w-[160px] font-semibold">Disk</TableHead>
<TableHead className="min-w-[100px] font-semibold">Uptime</TableHead>
<TableHead className="min-w-[160px] font-semibold">Last Checked</TableHead>
<TableHead className="min-w-[80px] text-center font-semibold">Actions</TableHead>
<TableHead className="min-w-[200px] font-semibold">{t('container', 'docker')}</TableHead>
<TableHead className="min-w-[100px] font-semibold">{t('status', 'docker')}</TableHead>
<TableHead className="min-w-[140px] font-semibold">{t('cpuUsage', 'docker')}</TableHead>
<TableHead className="min-w-[160px] font-semibold">{t('memory', 'docker')}</TableHead>
<TableHead className="min-w-[160px] font-semibold">{t('disk', 'docker')}</TableHead>
<TableHead className="min-w-[100px] font-semibold">{t('uptime', 'docker')}</TableHead>
<TableHead className="min-w-[160px] font-semibold">{t('lastChecked', 'docker')}</TableHead>
<TableHead className="min-w-[80px] text-center font-semibold">{t('actions', 'docker')}</TableHead>
</TableRow>
</TableHeader>
);
@@ -2,6 +2,7 @@
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search, RefreshCw } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface DockerTableSearchProps {
searchTerm: string;
@@ -11,12 +12,13 @@ interface DockerTableSearchProps {
}
export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoading }: DockerTableSearchProps) => {
const { t } = useLanguage();
return (
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search containers..."
placeholder={t('searchContainersPlaceholder', 'docker')}
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 sm:w-64 bg-background border-border"
@@ -30,7 +32,7 @@ export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoa
className="w-full sm:w-auto bg-background border-border hover:bg-muted"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
<span className="sm:inline">Refresh</span>
<span className="sm:inline">{t('refresh', 'docker')}</span>
</Button>
</div>
);
@@ -11,6 +11,7 @@ import { Plus, X, Server } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
import { useLanguage } from "@/contexts/LanguageContext";
interface ComponentsSelectorProps {
selectedComponents: Partial<StatusPageComponentRecord>[];
@@ -19,6 +20,7 @@ interface ComponentsSelectorProps {
}
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
const { t } = useLanguage();
const [showAddForm, setShowAddForm] = useState(false);
const [newComponent, setNewComponent] = useState({
name: '',
@@ -71,16 +73,16 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Status Page Components
{t('statusPageComponents')}
</CardTitle>
<CardDescription>
Add monitoring components like uptime services, SSL certificates, and incident tracking
{t('addMonitoringComponentsDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedComponents.length > 0 && (
<div className="space-y-2">
<Label>Selected Components</Label>
<Label>{t('selectedComponents')}</Label>
<div className="space-y-2">
{selectedComponents.map((component, index) => (
<div key={component.id || index} className="flex items-center justify-between p-3 border rounded-lg">
@@ -92,12 +94,12 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<div className="flex gap-2 mt-1">
{component.service_id && (
<Badge variant="secondary" className="text-xs">
Service: {services.find(s => s.id === component.service_id)?.name || component.service_id}
{t('service')}: {services.find(s => s.id === component.service_id)?.name || component.service_id}
</Badge>
)}
{component.server_id && (
<Badge variant="secondary" className="text-xs">
Server: {component.server_id}
{t('server')}: {component.server_id}
</Badge>
)}
</div>
@@ -122,22 +124,22 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
Add Component
{t('addComponent')}
</Button>
) : (
<div className="border rounded-lg p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="component-name">Component Name</Label>
<Label htmlFor="component-name">{t('componentName')}</Label>
<Input
id="component-name"
placeholder="e.g., Main Website"
placeholder={t('componentNamePlaceholder')}
value={newComponent.name}
onChange={(e) => setNewComponent({ ...newComponent, name: e.target.value })}
/>
</div>
<div>
<Label htmlFor="display-order">Display Order</Label>
<Label htmlFor="display-order">{t('displayOrder')}</Label>
<Input
id="display-order"
type="number"
@@ -148,10 +150,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</div>
<div>
<Label htmlFor="component-description">Description (Optional)</Label>
<Label htmlFor="component-description">{t('descriptionOptional')}</Label>
<Textarea
id="component-description"
placeholder="Brief description of this component"
placeholder={t('descriptionPlaceholder')}
value={newComponent.description}
onChange={(e) => setNewComponent({ ...newComponent, description: e.target.value })}
/>
@@ -159,10 +161,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="service-id">Uptime Service (Optional)</Label>
<Label htmlFor="service-id">{t('uptimeServiceOptional')}</Label>
<Select onValueChange={(value) => setNewComponent({ ...newComponent, service_id: value })}>
<SelectTrigger>
<SelectValue placeholder="Select an uptime service" />
<SelectValue placeholder={t('selectUptimeService')} />
</SelectTrigger>
<SelectContent>
{services.map((service) => (
@@ -181,10 +183,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</Select>
</div>
<div>
<Label htmlFor="server-id">Server ID (Optional)</Label>
<Label htmlFor="server-id">{t('serverIdOptional')}</Label>
<Input
id="server-id"
placeholder="server_456"
placeholder={t('serverIdPlaceholder')}
value={newComponent.server_id}
onChange={(e) => setNewComponent({ ...newComponent, server_id: e.target.value })}
/>
@@ -193,10 +195,10 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<div className="flex gap-2">
<Button onClick={addComponent} disabled={!newComponent.name.trim()}>
Add Component
{t('addComponent')}
</Button>
<Button variant="outline" onClick={() => setShowAddForm(false)}>
Cancel
{t('cancel')}
</Button>
</div>
</div>
@@ -14,6 +14,7 @@ import { useCreateStatusPageComponent } from '@/hooks/useStatusPageComponents';
import { ComponentsSelector } from './ComponentsSelector';
import { Plus } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({
title: z.string().min(1, 'Title is required'),
@@ -31,6 +32,7 @@ const formSchema = z.object({
type FormData = z.infer<typeof formSchema>;
export const CreateOperationalPageDialog = () => {
const { t } = useLanguage();
const [open, setOpen] = useState(false);
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
const createMutation = useCreateOperationalPage();
@@ -102,14 +104,14 @@ export const CreateOperationalPageDialog = () => {
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Create Page
{t('createPage')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create Operational Page</DialogTitle>
<DialogTitle>{t('createOperationalPage')}</DialogTitle>
<DialogDescription>
Create a new operational status page to monitor your services and components.
{t('createOperationalPageDesc')}
</DialogDescription>
</DialogHeader>
@@ -121,9 +123,9 @@ export const CreateOperationalPageDialog = () => {
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormLabel>{t('title')}</FormLabel>
<FormControl>
<Input placeholder="My Service Status" {...field} />
<Input placeholder={t('myServiceStatusPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -135,9 +137,9 @@ export const CreateOperationalPageDialog = () => {
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormLabel>{t('slug')}</FormLabel>
<FormControl>
<Input placeholder="my-service-status" {...field} />
<Input placeholder={t('myServiceStatusSlugPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -150,10 +152,10 @@ export const CreateOperationalPageDialog = () => {
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormLabel>{t('description')}</FormLabel>
<FormControl>
<Textarea
placeholder="A brief description of your operational page"
placeholder={t('operationalPageDescriptionPlaceholder')}
className="min-h-[80px]"
{...field}
/>
@@ -169,17 +171,17 @@ export const CreateOperationalPageDialog = () => {
name="theme"
render={({ field }) => (
<FormItem>
<FormLabel>Theme</FormLabel>
<FormLabel>{t('theme')}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select theme" />
<SelectValue placeholder={t('selectTheme')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="default">{t('themeDefault')}</SelectItem>
<SelectItem value="dark">{t('themeDark')}</SelectItem>
<SelectItem value="light">{t('themeLight')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -192,18 +194,18 @@ export const CreateOperationalPageDialog = () => {
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Initial Status</FormLabel>
<FormLabel>{t('initialStatus')}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select status" />
<SelectValue placeholder={t('selectStatus')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="operational">Operational</SelectItem>
<SelectItem value="degraded">Degraded Performance</SelectItem>
<SelectItem value="maintenance">Under Maintenance</SelectItem>
<SelectItem value="major_outage">Major Outage</SelectItem>
<SelectItem value="operational">{t('statusOperational')}</SelectItem>
<SelectItem value="degraded">{t('statusDegraded')}</SelectItem>
<SelectItem value="maintenance">{t('statusMaintenance')}</SelectItem>
<SelectItem value="major_outage">{t('statusMajorOutage')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -218,9 +220,9 @@ export const CreateOperationalPageDialog = () => {
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Public Page</FormLabel>
<FormLabel>{t('publicPage')}</FormLabel>
<FormDescription>
Make this page publicly accessible
{t('makePagePublic')}
</FormDescription>
</div>
<FormControl>
@@ -238,12 +240,12 @@ export const CreateOperationalPageDialog = () => {
name="custom_domain"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Domain (Optional)</FormLabel>
<FormLabel>{t('customDomainOptional')}</FormLabel>
<FormControl>
<Input placeholder="status.yourdomain.com" {...field} />
<Input placeholder={t('customDomainPlaceholder')} {...field} />
</FormControl>
<FormDescription>
Custom domain for your status page
{t('customDomainDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -261,13 +263,13 @@ export const CreateOperationalPageDialog = () => {
variant="outline"
onClick={() => setOpen(false)}
>
Cancel
{t('cancel')}
</Button>
<Button
type="submit"
disabled={createMutation.isPending || createComponentMutation.isPending}
>
{createMutation.isPending || createComponentMutation.isPending ? 'Creating...' : 'Create Page'}
{createMutation.isPending || createComponentMutation.isPending ? t('creating') : t('createPage')}
</Button>
</div>
</form>
@@ -14,6 +14,7 @@ import { useCreateStatusPageComponent, useStatusPageComponentsByOperationalId, u
import { ComponentsSelector } from './ComponentsSelector';
import { OperationalPageRecord } from '@/types/operational.types';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useLanguage } from "@/contexts/LanguageContext";
const formSchema = z.object({
title: z.string().min(1, 'Title is required'),
@@ -37,6 +38,7 @@ interface EditOperationalPageDialogProps {
}
export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOperationalPageDialogProps) => {
const { t } = useLanguage();
const [selectedComponents, setSelectedComponents] = useState<Partial<StatusPageComponentRecord>[]>([]);
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
const [componentsLoaded, setComponentsLoaded] = useState(false);
@@ -195,9 +197,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Operational Page</DialogTitle>
<DialogTitle>{t('editOperationalPage')}</DialogTitle>
<DialogDescription>
Update your operational status page settings and manage components.
{t('updateYourOperationalPage')}
</DialogDescription>
</DialogHeader>
@@ -209,9 +211,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormLabel>{t('title')}</FormLabel>
<FormControl>
<Input placeholder="My Service Status" {...field} />
<Input placeholder={t('myServiceStatusPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -223,9 +225,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormLabel>{t('slug')}</FormLabel>
<FormControl>
<Input placeholder="my-service-status" {...field} />
<Input placeholder={t('myServiceStatusSlugPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -238,10 +240,10 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormLabel>{t('description')}</FormLabel>
<FormControl>
<Textarea
placeholder="A brief description of your operational page"
placeholder={t('operationalPageDescriptionPlaceholder')}
className="min-h-[80px]"
{...field}
/>
@@ -257,17 +259,17 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="theme"
render={({ field }) => (
<FormItem>
<FormLabel>Theme</FormLabel>
<FormLabel>{t('theme')}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select theme" />
<SelectValue placeholder={t('selectTheme')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="default">{t('themeDefault')}</SelectItem>
<SelectItem value="dark">{t('themeDark')}</SelectItem>
<SelectItem value="light">{t('themeLight')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -280,18 +282,18 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<FormLabel>{t('status')}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select status" />
<SelectValue placeholder={t('selectStatus')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="operational">Operational</SelectItem>
<SelectItem value="degraded">Degraded Performance</SelectItem>
<SelectItem value="maintenance">Under Maintenance</SelectItem>
<SelectItem value="major_outage">Major Outage</SelectItem>
<SelectItem value="operational">{t('statusOperational')}</SelectItem>
<SelectItem value="degraded">{t('statusDegraded')}</SelectItem>
<SelectItem value="maintenance">{t('statusMaintenance')}</SelectItem>
<SelectItem value="major_outage">{t('statusMajorOutage')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -306,9 +308,9 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Public Page</FormLabel>
<FormLabel>{t('publicPage')}</FormLabel>
<FormDescription>
Make this page publicly accessible
{t('makePagePublic')}
</FormDescription>
</div>
<FormControl>
@@ -326,12 +328,12 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
name="custom_domain"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Domain (Optional)</FormLabel>
<FormLabel>{t('customDomainOptional')}</FormLabel>
<FormControl>
<Input placeholder="status.yourdomain.com" {...field} />
<Input placeholder={t('customDomainPlaceholder')} {...field} />
</FormControl>
<FormDescription>
Custom domain for your status page
{t('customDomainDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -350,13 +352,13 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
{t('cancel')}
</Button>
<Button
type="submit"
disabled={isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending}
>
{isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending ? 'Updating...' : 'Update Page'}
{isFormSubmitting || updateMutation.isPending || createComponentMutation.isPending ? t('updating') : t('updatePage')}
</Button>
</div>
</form>
@@ -6,6 +6,7 @@ import { OperationalPageRecord } from '@/types/operational.types';
import { StatusBadge } from './StatusBadge';
import { Globe, ExternalLink, Eye, Settings, Trash2 } from 'lucide-react';
import { format } from 'date-fns';
import { useLanguage } from "@/contexts/LanguageContext";
interface OperationalPageCardProps {
page: OperationalPageRecord;
@@ -15,6 +16,8 @@ interface OperationalPageCardProps {
}
export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: OperationalPageCardProps) => {
const { t } = useLanguage();
return (
<Card className="hover:shadow-lg transition-shadow duration-200">
<CardHeader className="pb-3">
@@ -32,23 +35,23 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-muted-foreground">Slug:</span>
<span className="font-medium text-muted-foreground">{t('slug')}:</span>
<p className="mt-1">{page.slug}</p>
</div>
<div>
<span className="font-medium text-muted-foreground">Theme:</span>
<span className="font-medium text-muted-foreground">{t('theme')}:</span>
<p className="mt-1 capitalize">{page.theme}</p>
</div>
<div>
<span className="font-medium text-muted-foreground">Public:</span>
<span className="font-medium text-muted-foreground">{t('public')}:</span>
<div className="mt-1">
<Badge variant={page.is_public === 'true' ? 'default' : 'secondary'}>
{page.is_public === 'true' ? 'Yes' : 'No'}
{page.is_public === 'true' ? t('yes') : t('no')}
</Badge>
</div>
</div>
<div>
<span className="font-medium text-muted-foreground">Updated:</span>
<span className="font-medium text-muted-foreground">{t('updated')}:</span>
<p className="mt-1">{format(new Date(page.updated), 'MMM dd, yyyy')}</p>
</div>
</div>
@@ -70,7 +73,7 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
className="flex-1"
>
<Eye className="h-4 w-4 mr-2" />
View
{t('view')}
</Button>
)}
{onEdit && (
@@ -81,7 +84,7 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
className="flex-1"
>
<Settings className="h-4 w-4 mr-2" />
Edit
{t('edit')}
</Button>
)}
{onDelete && (
@@ -9,6 +9,8 @@ import { Button } from '@/components/ui/button';
import { OperationalPageRecord } from '@/types/operational.types';
import { Activity, Plus, RefreshCw } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { useLanguage } from "@/contexts/LanguageContext";
import {
AlertDialog,
AlertDialogAction,
@@ -21,6 +23,7 @@ import {
} from '@/components/ui/alert-dialog';
export const OperationalPageContent = () => {
const { t } = useLanguage();
const navigate = useNavigate();
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
const deleteMutation = useDeleteOperationalPage();
@@ -68,13 +71,13 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
<h3 className="text-lg font-semibold mb-2">{t('failedToLoadOperationalPages')}</h3>
<p className="text-muted-foreground mb-4">
There was an error loading your operational pages. Please try again.
{t('loadingoperationalPages')}
</p>
<Button onClick={() => refetch()} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Try Again
{t('tryagain')}
</Button>
</div>
</div>
@@ -86,9 +89,9 @@ export const OperationalPageContent = () => {
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
<h1 className="text-3xl font-bold tracking-tight mb-2"> {t('operationalPages')}</h1>
<p className="text-muted-foreground">
Manage your public status pages and monitor service health
{t('describeOperation')}
</p>
</div>
@@ -100,7 +103,7 @@ export const OperationalPageContent = () => {
disabled={isRefetching}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
Refresh
{t('refresh')}
</Button>
<CreateOperationalPageDialog />
</div>
@@ -135,9 +138,9 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
<h3 className="text-lg font-semibold mb-2">{t('noOperationalPagesFound')}</h3>
<p className="text-muted-foreground mb-6">
Create your first operational page to start monitoring your services and communicate status to your users.
{t('createYourFirstOperationalPage')}
</p>
<CreateOperationalPageDialog />
</CardContent>
@@ -164,14 +167,14 @@ export const OperationalPageContent = () => {
<div className="mt-8 pt-6 border-t">
<div className="flex flex-wrap gap-6 text-sm text-muted-foreground">
<div>
<span className="font-medium">Total Pages:</span> {pages.length}
<span className="font-medium">{t('totalPages')}:</span> {pages.length}
</div>
<div>
<span className="font-medium">Public Pages:</span>{' '}
<span className="font-medium">{t('totalPages')}:</span>{' '}
{pages.filter(p => p.is_public === 'true').length}
</div>
<div>
<span className="font-medium">Operational:</span>{' '}
<span className="font-medium">{t('operational')}:</span>{' '}
{pages.filter(p => p.status === 'operational').length}
</div>
</div>
@@ -189,19 +192,19 @@ export const OperationalPageContent = () => {
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
<AlertDialogTitle>{t('deleteOperationalPage')}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone and will permanently remove the operational page and all its components.
{t('deleteOperationalPageConfirm').replace('{title}', pageToDelete?.title ?? '')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
{deleteMutation.isPending ? t('deleting') : t('delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -5,6 +5,7 @@ import { format } from 'date-fns';
import { OperationalPageRecord } from '@/types/operational.types';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { Service } from '@/types/service.types';
import { useLanguage } from '@/contexts/LanguageContext';
interface CurrentStatusSectionProps {
page: OperationalPageRecord;
@@ -45,18 +46,18 @@ const getActualStatus = (components: StatusPageComponentRecord[], services: Serv
return 'operational';
};
const getStatusMessage = (status: OperationalPageRecord['status']) => {
const getStatusMessage = (status: OperationalPageRecord['status'], t: (k: string, m?: string) => string) => {
switch (status) {
case 'operational':
return 'All systems are operational';
return t('allOperational', 'public');
case 'degraded':
return 'Some systems are experiencing degraded performance';
return t('degradedPerformance', 'public');
case 'maintenance':
return 'Systems are currently under maintenance';
return t('underMaintenance', 'public');
case 'major_outage':
return 'We are experiencing a major service outage';
return t('majorOutage', 'public');
default:
return 'Status unknown';
return t('statusUnknown', 'public');
}
};
@@ -106,6 +107,7 @@ const getStatusBackground = (status: OperationalPageRecord['status']) => {
};
export const CurrentStatusSection = ({ page, components, services }: CurrentStatusSectionProps) => {
const { t } = useLanguage();
const actualStatus = getActualStatus(components, services);
const displayStatus = actualStatus; // Use actual status for real-time accuracy
@@ -114,7 +116,7 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
<CardHeader>
<CardTitle className="flex items-center gap-3 text-card-foreground text-xl">
<Shield className="h-6 w-6" />
System Status
{t('systemStatus', 'public')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
@@ -123,10 +125,10 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
{getStatusIcon(displayStatus)}
<div>
<h3 className={`text-2xl font-bold ${getStatusColor(displayStatus)}`}>
{getStatusMessage(displayStatus)}
{getStatusMessage(displayStatus, t)}
</h3>
<p className="text-sm text-muted-foreground mt-1">
Status automatically updated based on component health
{t('autoUpdatedByHealth', 'public')}
</p>
</div>
</div>
@@ -136,20 +138,20 @@ export const CurrentStatusSection = ({ page, components, services }: CurrentStat
displayStatus === 'maintenance' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
}`}>
{displayStatus === 'operational' ? 'All Systems Operational' :
displayStatus === 'degraded' ? 'Degraded Performance' :
displayStatus === 'maintenance' ? 'Under Maintenance' : 'Major Outage'}
{displayStatus === 'operational' ? t('allOperational', 'public') :
displayStatus === 'degraded' ? t('degradedPerformance', 'public') :
displayStatus === 'maintenance' ? t('underMaintenance', 'public') : t('majorOutage', 'public')}
</div>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground border-t pt-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>Last updated: {format(new Date(), 'MMM dd, yyyy HH:mm')} UTC</span>
<span>{t('lastUpdatedAt', 'public', { time: format(new Date(), 'MMM dd, yyyy HH:mm') })}</span>
</div>
<div className="flex items-center gap-2">
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
<span>Live status monitoring</span>
<span>{t('liveStatusMonitoring', 'public')}</span>
</div>
</div>
</CardContent>
@@ -9,8 +9,10 @@ import { CurrentStatusSection } from './CurrentStatusSection';
import { ComponentsStatusSection } from './ComponentsStatusSection';
import { OverallUptimeSection } from './OverallUptimeSection';
import { PublicStatusPageFooter } from './PublicStatusPageFooter';
import { useLanguage } from '@/contexts/LanguageContext';
export const PublicStatusPage = () => {
const { t } = useLanguage();
const { slug } = useParams<{ slug: string }>();
// console.log('PublicStatusPage - slug from params:', slug);
@@ -59,9 +61,9 @@ export const PublicStatusPage = () => {
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<div className="space-y-2">
<p className="text-lg font-medium text-foreground">Loading Status Page</p>
<p className="text-sm text-muted-foreground">Fetching real-time system status...</p>
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
<p className="text-lg font-medium text-foreground">{t('loadingStatusPage', 'public')}</p>
<p className="text-sm text-muted-foreground">{t('fetchingRealtimeStatus', 'public')}</p>
<p className="text-xs text-muted-foreground">{t('slugLabel', 'public')}: {slug || 'No slug provided'}</p>
</div>
</div>
</div>
@@ -76,19 +78,19 @@ export const PublicStatusPage = () => {
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
</div>
<div className="space-y-2">
<h1 className="text-2xl font-bold text-foreground">Status Page Not Found</h1>
<h1 className="text-2xl font-bold text-foreground">{t('statusPageNotFound', 'public')}</h1>
<p className="text-muted-foreground">
{error || 'The requested status page could not be found or is not publicly accessible.'}
{error || t('notFoundDescription', 'public')}
</p>
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
<p className="text-xs text-muted-foreground">{t('slugLabel', 'public')}: {slug || 'No slug provided'}</p>
</div>
<div className="flex gap-3 justify-center">
<Button onClick={() => window.history.back()} variant="outline">
Go Back
{t('goBack', 'public')}
</Button>
<Button onClick={() => window.location.reload()} className="gap-2">
<RefreshCw className="h-4 w-4" />
Retry
{t('retry', 'public')}
</Button>
</div>
</div>
@@ -10,6 +10,7 @@ import { Copy, Download, Terminal, CheckCircle, Zap, Play } from "lucide-react";
import { regionalService } from "@/services/regionalService";
import { InstallCommand } from "@/types/regional.types";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
interface AddRegionalAgentDialogProps {
open: boolean;
@@ -22,6 +23,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
onOpenChange,
onAgentAdded
}) => {
const { t } = useLanguage();
const [step, setStep] = useState(1);
const [regionName, setRegionName] = useState("");
const [agentIp, setAgentIp] = useState("");
@@ -44,8 +46,8 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
setStep(2);
} catch (error) {
toast({
title: "Error",
description: "Failed to create regional agent configuration.",
title: t('error'),
description: t('failedToCreateAgent'),
variant: "destructive",
});
} finally {
@@ -59,7 +61,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
toast({
title: "Copied!",
title: t('copied'),
description: `${description} copied to clipboard.`,
});
return;
@@ -80,8 +82,9 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
if (successful) {
toast({
title: "Copied!",
title: t('copied'),
description: `${description} copied to clipboard.`,
//description: t('copiedDescription', { description }),
});
} else {
throw new Error('execCommand failed');
@@ -93,15 +96,15 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
const userAgent = navigator.userAgent.toLowerCase();
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
let errorMessage = "Failed to copy to clipboard.";
let errorMessage = t('copyFailedDescription');
if (isLocalhost) {
errorMessage += " This is common in local development. Please manually copy the text.";
errorMessage += " " + t('copyFailedLocalhost');
} else if (userAgent.includes('chrome')) {
errorMessage += " Try using HTTPS or enable clipboard permissions.";
errorMessage += " " + t('copyFailedChrome');
}
toast({
title: "Copy failed",
title: t('copyFailed'),
description: errorMessage,
variant: "destructive",
});
@@ -133,8 +136,8 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
URL.revokeObjectURL(url);
toast({
title: "Downloaded!",
description: "Installation script downloaded successfully.",
title: t('downloaded'),
description: t('downloadedDescription'),
});
};
@@ -158,9 +161,9 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
<DialogHeader>
<DialogTitle>Add Regional Monitoring Agent</DialogTitle>
<DialogTitle>{t('addRegionalMonitoringAgent')}</DialogTitle>
<DialogDescription>
Deploy a regional monitoring agent with automatic one-click installation.
{t('deployRegionalMonitoringAgent')}
</DialogDescription>
</DialogHeader>
@@ -168,10 +171,10 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
<form onSubmit={handleCreateAgent} className="space-y-6">
<div className="space-y-4">
<div>
<Label htmlFor="regionName">Region Name</Label>
<Label htmlFor="regionName">{t('regionName')}</Label>
<Input
id="regionName"
placeholder="e.g., us-east-1, europe-west-1, asia-pacific-1"
placeholder={t('regionNamePlaceholder')}
value={regionName}
onChange={(e) => setRegionName(e.target.value)}
required
@@ -179,10 +182,10 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
</div>
<div>
<Label htmlFor="agentIp">Agent Server IP Address</Label>
<Label htmlFor="agentIp">{t('agentServerIpAddress')}</Label>
<Input
id="agentIp"
placeholder="e.g., 192.168.1.100 or your-server.example.com"
placeholder={t('agentIpPlaceholder')}
value={agentIp}
onChange={(e) => setAgentIp(e.target.value)}
required
@@ -192,10 +195,10 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
{t('cancel')}
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Generating..." : "Generate Installation"}
{isSubmitting ? t('generating') : t('generateInstallation')}
</Button>
</div>
</form>
@@ -205,17 +208,17 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
<div className="space-y-6">
<div className="text-center space-y-2">
<CheckCircle className="h-12 w-12 text-green-600 mx-auto" />
<h3 className="text-lg font-semibold">Agent Configuration Ready!</h3>
<h3 className="text-lg font-semibold">{t('agentConfigurationReady')}</h3>
<p className="text-muted-foreground">
One-click installation script generated with automatic configuration.
{t('oneClickInstallScriptGenerated')}
</p>
</div>
<Tabs defaultValue="oneclicK" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="oneclicK">One-Click Install</TabsTrigger>
<TabsTrigger value="details">Agent Details</TabsTrigger>
<TabsTrigger value="manual">Manual Install</TabsTrigger>
<TabsTrigger value="oneclicK">{t('oneClickInstallTab')}</TabsTrigger>
<TabsTrigger value="details">{t('agentDetailsTab')}</TabsTrigger>
<TabsTrigger value="manual">{t('manualInstallTab')}</TabsTrigger>
</TabsList>
<TabsContent value="oneclicK" className="space-y-4">
@@ -223,29 +226,29 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
One-Click Automatic Installation
{t('oneClickAutomaticInstallation')}
</CardTitle>
<CardDescription>
Complete installation, configuration, and service startup in one command
{t('completeInstallationDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-green-800 font-medium mb-2">
<Play className="h-4 w-4" />
What this script does automatically:
{t('whatThisScriptDoes')}
</div>
<ul className="text-sm text-green-700 space-y-1 ml-6">
<li> Downloads the latest .deb package</li>
<li> Installs the regional monitoring agent</li>
<li> Creates configuration file with your settings</li>
<li> Starts and enables the service</li>
<li> Runs health checks</li>
<li> {t('scriptActionDownload')}</li>
<li> {t('scriptActionInstall')}</li>
<li> {t('scriptActionConfigure')}</li>
<li> {t('scriptActionStart')}</li>
<li> {t('scriptActionHealthChecks')}</li>
</ul>
</div>
<div className="space-y-2">
<Label>Run this command on your target server:</Label>
<Label>{t('runCommandOnServer')}</Label>
<div className="relative">
<Textarea
readOnly
@@ -272,7 +275,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
<div className="flex gap-2">
<Button onClick={downloadScript} variant="outline" className="flex-1">
<Download className="mr-2 h-4 w-4" />
Download Complete Script
{t('downloadCompleteScript')}
</Button>
<Button
onClick={() => copyToClipboard(installCommand.bash_script, "Installation script")}
@@ -280,17 +283,14 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
className="flex-1"
>
<Copy className="mr-2 h-4 w-4" />
Copy Full Script
{t('copyFullScript')}
</Button>
</div>
{/* Local development notice */}
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
<strong>Local Development Notice:</strong> If clipboard copying fails, this is normal in local development.
You can use the "Download Complete Script" button instead or manually copy the text.
</p>
<p className="text-sm text-yellow-800" dangerouslySetInnerHTML={{ __html: t('localDevelopmentNotice') }} />
</div>
)}
</CardContent>
@@ -300,15 +300,15 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
<TabsContent value="details" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Generated Agent Configuration</CardTitle>
<CardTitle>{t('generatedAgentConfiguration')}</CardTitle>
<CardDescription>
These values will be automatically configured during installation
{t('autoConfiguredDuringInstallation')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-foreground">Agent ID</Label>
<Label className="text-sm font-medium text-foreground">{t('agentId')}</Label>
<div className="relative">
<Input
value={installCommand.agent_id}
@@ -319,14 +319,14 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.agent_id, "Agent ID")}
onClick={() => copyToClipboard(installCommand.agent_id, t('agentId'))}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Region Name</Label>
<Label className="text-sm font-medium text-foreground">{t('regionNameDetail')}</Label>
<Input
value={regionName}
readOnly
@@ -334,7 +334,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground">Server IP</Label>
<Label className="text-sm font-medium text-foreground">{t('serverIp')}</Label>
<Input
value={agentIp}
readOnly
@@ -342,7 +342,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground">API Endpoint</Label>
<Label className="text-sm font-medium text-foreground">{t('apiEndpoint')}</Label>
<Input
value={installCommand.api_endpoint}
readOnly
@@ -352,7 +352,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
</div>
<div>
<Label className="text-sm font-medium text-foreground">Authentication Token</Label>
<Label className="text-sm font-medium text-foreground">{t('authenticationToken')}</Label>
<div className="relative">
<Input
value={installCommand.token}
@@ -363,7 +363,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.token, "Authentication token")}
onClick={() => copyToClipboard(installCommand.token, t('authenticationToken'))}
>
<Copy className="h-3 w-3" />
</Button>
@@ -371,9 +371,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
</div>
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Configuration file location:</strong> /etc/regional-check-agent/regional-check-agent.env
</p>
<p className="text-sm text-blue-800" dangerouslySetInnerHTML={{ __html: t('configurationFileLocation') }} />
</div>
</CardContent>
</Card>
@@ -384,28 +382,30 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
Manual Installation Steps
{t('manualInstallationSteps')}
</CardTitle>
<CardDescription>
Step-by-step manual installation process
{t('stepByStepManualInstallation')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-sm space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 1: Download Package</p>
<p className="font-medium">{t('step1DownloadPackage')}</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
<p className="text-xs text-muted-foreground mt-1"> If youre using the amd64 architecture, please download this package. </p> <br/>
<p className="text-xs text-muted-foreground mt-1">{t('downloadAmd64Notice')}</p>
<br/>
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb
</code>
<code className="text-xs bg-muted p-2 rounded block mt-1">
<p className="text-xs text-muted-foreground mt-1"> If youre using the arm64 architecture, please download this package. </p> <br/>
<p className="text-xs text-muted-foreground mt-1">{t('downloadArm64Notice')}</p>
<br/>
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb
</code>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 2: Install Package</p>
<p className="font-medium">{t('step2InstallPackage')}</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb<br/>
sudo apt-get install -f
@@ -413,18 +413,18 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 3: Configure Agent</p>
<p className="font-medium">{t('step3ConfigureAgent')}</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo nano /etc/regional-check-agent/regional-check-agent.env
</code>
<code className="text-xs bg-muted p-2 rounded block mt-1">
Copy sample configuration from https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf
{t('copySampleConfiguration')}
</code>
<p className="text-xs text-muted-foreground mt-1">Add the configuration values shown in the Agent Details tab</p>
<p className="text-xs text-muted-foreground mt-1">{t('addConfigurationValues')}</p>
</div>
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-medium">Step 4: Start Service</p>
<p className="font-medium">{t('step4StartService')}</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo systemctl enable regional-check-agent<br/>
sudo systemctl start regional-check-agent
@@ -432,7 +432,7 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
</div>
<div className="border-l-4 border-green-500 pl-4">
<p className="font-medium">Step 5: Verify Installation</p>
<p className="font-medium">{t('step5VerifyInstallation')}</p>
<code className="text-xs bg-muted p-2 rounded block mt-1">
sudo systemctl status regional-check-agent<br/>
curl http://localhost:8091/health
@@ -446,10 +446,10 @@ curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/s
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={resetDialog}>
Add Another Agent
{t('addAnotherAgent')}
</Button>
<Button onClick={handleComplete}>
Complete Setup
{t('completeSetup')}
</Button>
</div>
</div>
@@ -8,6 +8,7 @@ import { MapPin, Wifi, WifiOff, MoreVertical, Trash2, Terminal, Copy } from "luc
import { RegionalService } from "@/types/regional.types";
import { formatDistanceToNow } from "date-fns";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
interface RegionalAgentCardProps {
agent: RegionalService;
@@ -16,6 +17,7 @@ interface RegionalAgentCardProps {
export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onDelete }) => {
const { toast } = useToast();
const { t } = useLanguage();
// Check if this is the default agent that cannot be removed
const isDefaultAgent = agent.agent_id === "1" || agent.region_name === "Default";
@@ -24,13 +26,13 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
try {
await navigator.clipboard.writeText(agent.agent_id);
toast({
title: "Copied!",
description: "Agent ID copied to clipboard.",
title: t('copied'),
description: t('copiedDescription'),
});
} catch (error) {
toast({
title: "Copy failed",
description: "Failed to copy agent ID.",
title: t('copyFailed'),
description: t('copyFailedDescription'),
variant: "destructive",
});
}
@@ -40,14 +42,14 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
if (agent.connection === 'online') {
return {
icon: <Wifi className="h-4 w-4" />,
label: 'Online',
label: t('online'),
variant: 'default' as const,
className: 'bg-green-100 text-green-800 hover:bg-green-100'
};
} else {
return {
icon: <WifiOff className="h-4 w-4" />,
label: 'Offline',
label: t('offline'),
variant: 'secondary' as const,
className: 'bg-red-100 text-red-800 hover:bg-red-100'
};
@@ -67,7 +69,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
{agent.region_name}
{isDefaultAgent && (
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
Default
{t('defaultBadge')}
</Badge>
)}
</CardTitle>
@@ -86,12 +88,12 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={copyAgentId}>
<Copy className="mr-2 h-4 w-4" />
Copy Agent ID
{t('copyAgentId')}
</DropdownMenuItem>
{!isDefaultAgent && (
<DropdownMenuItem onClick={onDelete} className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Remove Agent
{t('removeAgent')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
@@ -116,12 +118,12 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Agent ID:</span>
<span className="text-muted-foreground">{t('agentId')}</span>
<span className="font-mono text-xs">{agent.agent_id.substring(0, 12)}...</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Last Updated:</span>
<span className="text-muted-foreground">{t('lastUpdated')}</span>
<span className="text-xs">
{formatDistanceToNow(new Date(agent.updated), { addSuffix: true })}
</span>
@@ -132,7 +134,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<div className="pt-2 border-t">
<div className="flex items-center text-xs text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse"></div>
Active monitoring
{t('activeMonitoring')}
</div>
</div>
)}
@@ -141,7 +143,7 @@ export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onD
<div className="pt-2 border-t">
<div className="flex items-center text-xs text-red-600">
<div className="w-2 h-2 bg-red-600 rounded-full mr-2"></div>
Connection lost
{t('connectionLost')}
</div>
</div>
)}
@@ -9,8 +9,10 @@ import { RegionalService } from "@/types/regional.types";
import { AddRegionalAgentDialog } from "./AddRegionalAgentDialog";
import { RegionalAgentCard } from "./RegionalAgentCard";
import { useToast } from "@/hooks/use-toast";
import { useLanguage } from "@/contexts/LanguageContext";
export const RegionalMonitoringContent = () => {
const { t } = useLanguage();
const [addDialogOpen, setAddDialogOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
@@ -24,8 +26,8 @@ export const RegionalMonitoringContent = () => {
const handleAgentAdded = () => {
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({
title: "Regional Agent Added",
description: "The regional monitoring agent has been successfully configured.",
title: t('regionalAgentAdded'),
description: t('regionalAgentAddedDesc'),
});
};
@@ -34,13 +36,13 @@ export const RegionalMonitoringContent = () => {
await regionalService.deleteRegionalService(id);
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
toast({
title: "Agent Removed",
description: "The regional monitoring agent has been removed.",
title: t('agentRemoved'),
description: t('agentRemovedDesc'),
});
} catch (error) {
toast({
title: "Error",
description: "Failed to remove the regional monitoring agent.",
title: t('error'),
description: t('failedToRemoveAgent'),
variant: "destructive",
});
}
@@ -53,14 +55,14 @@ export const RegionalMonitoringContent = () => {
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
<h1 className="text-3xl font-bold tracking-tight">{t('regionalmonitoring')}</h1>
<p className="text-muted-foreground">
Manage distributed monitoring agents across different regions
{t('descriptRegionPage')}
</p>
</div>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Add Regional Agent
{t('addRegionalAgent')}
</Button>
</div>
@@ -68,39 +70,39 @@ export const RegionalMonitoringContent = () => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
<CardTitle className="text-sm font-medium">{t('totalAgents')}</CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalAgents}</div>
<p className="text-xs text-muted-foreground">
Regional monitoring agents
{t('regionalMonitoringAgents')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Online Agents</CardTitle>
<CardTitle className="text-sm font-medium">{t('onlineAgents')}</CardTitle>
<Wifi className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
<p className="text-xs text-muted-foreground">
Currently connected
{t('currentlyConnected')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Offline Agents</CardTitle>
<CardTitle className="text-sm font-medium">{t('offlineAgents')}</CardTitle>
<WifiOff className="h-4 w-4 text-red-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
<p className="text-xs text-muted-foreground">
Disconnected agents
{t('disconnectedAgents')}
</p>
</CardContent>
</Card>
@@ -108,7 +110,7 @@ export const RegionalMonitoringContent = () => {
{/* Agents List */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Regional Agents</h2>
<h2 className="text-xl font-semibold">{t('regionalAgents')}</h2>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -131,13 +133,13 @@ export const RegionalMonitoringContent = () => {
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
<h3 className="text-lg font-semibold mb-2">{t('noRegionalAgents')}</h3>
<p className="text-muted-foreground text-center mb-4">
Get started by adding your first regional monitoring agent to extend your monitoring coverage.
{t('getStartedAddAgent')}
</p>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Add First Agent
{t('addFirstAgent')}
</Button>
</CardContent>
</Card>
@@ -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>
@@ -158,14 +158,14 @@ const DataRetentionSettings = () => {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
{t("dataRetention", "settings")}
{t("dataRetention")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<AlertTriangle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
<span className="font-medium">{t("permissionNotice")}</span> {t("permissionNoticeDataRetention")}
</AlertDescription>
</Alert>
</CardContent>
@@ -178,7 +178,7 @@ const DataRetentionSettings = () => {
return (
<div className="p-4 flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
Loading retention settings...
{t("loadingRetentionSettings")}
</div>
);
}
@@ -189,16 +189,16 @@ const DataRetentionSettings = () => {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Data Retention Settings
{t("dataRetention")}
</CardTitle>
<CardDescription>
Configure how long monitoring data is kept in the system
{t("dataRetentionDescription")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div>
<Label htmlFor="uptimeRetention">Uptime Monitoring Retention (days)</Label>
<Label htmlFor="uptimeRetention">{t("uptimeRetentionLabel")}</Label>
<Input
id="uptimeRetention"
type="number"
@@ -218,12 +218,12 @@ const DataRetentionSettings = () => {
className="mt-1"
/>
<p className="text-sm text-muted-foreground mt-1">
Service uptime and incident data older than this will be automatically deleted
{t("uptimeRetentionHelp")}
</p>
</div>
<div>
<Label htmlFor="serverRetention">Server Monitoring Retention (days)</Label>
<Label htmlFor="serverRetention">{t("serverRetentionLabel")}</Label>
<Input
id="serverRetention"
type="number"
@@ -243,7 +243,7 @@ const DataRetentionSettings = () => {
className="mt-1"
/>
<p className="text-sm text-muted-foreground mt-1">
Server metrics and process data older than this will be automatically deleted
{t("serverRetentionHelp")}
</p>
</div>
</div>
@@ -252,7 +252,7 @@ const DataRetentionSettings = () => {
<Alert>
<Database className="h-4 w-4" />
<AlertDescription>
Last automatic cleanup: {new Date(lastCleanup).toLocaleString()}
{t("lastCleanup")}: {new Date(lastCleanup).toLocaleString()}
</AlertDescription>
</Alert>
)}
@@ -266,7 +266,7 @@ const DataRetentionSettings = () => {
{isSaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Save Changes
{t("save")}
</Button>
</CardFooter>
</Card>
@@ -247,6 +247,7 @@ export const NotificationChannelDialog = ({
onClose,
editingConfig
}: NotificationChannelDialogProps) => {
const { t } = useLanguage();
const isEditing = !!editingConfig;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
@@ -325,7 +326,7 @@ export const NotificationChannelDialog = ({
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{isEditing ? "Edit Notification Channel" : "Add Notification Channel"}
{isEditing ? t("editChannel") : t("addChannelDialog")}
</DialogTitle>
</DialogHeader>
@@ -336,28 +337,28 @@ export const NotificationChannelDialog = ({
name="notify_name"
render={({ field }) => (
<FormItem>
<FormLabel>Channel Name</FormLabel>
<FormLabel>{t("channelName")}</FormLabel>
<FormControl>
<Input placeholder="My Notification Channel" {...field} />
<Input placeholder={t("channelNamePlaceholder")} {...field} />
</FormControl>
<FormDescription>
A name to identify this notification channel
{t("channelNameDesc")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notification_type"
render={({ field }) => (
<FormItem>
<FormLabel>Channel Type</FormLabel>
<FormLabel>{t("channelType")}</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select notification type" />
<SelectValue placeholder={t("selectType")} />
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -370,8 +371,8 @@ export const NotificationChannelDialog = ({
className="w-5 h-5 object-contain"
/>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
<span className="font-medium">{t(option.value)}</span>
<span className="text-xs text-muted-foreground">{t(option.description)}</span>
</div>
</div>
</SelectItem>
@@ -382,7 +383,7 @@ export const NotificationChannelDialog = ({
</FormItem>
)}
/>
{notificationType === "telegram" && (
<>
<FormField
@@ -390,12 +391,12 @@ export const NotificationChannelDialog = ({
name="telegram_chat_id"
render={({ field }) => (
<FormItem>
<FormLabel>Chat ID</FormLabel>
<FormLabel>{t("telegramChatId")}</FormLabel>
<FormControl>
<Input placeholder="Telegram Chat ID" {...field} />
<Input placeholder={t("telegramChatIdPlaceholder")} {...field} />
</FormControl>
<FormDescription>
The Telegram chat ID to send notifications to
{t("telegramChatIdDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -406,12 +407,12 @@ export const NotificationChannelDialog = ({
name="bot_token"
render={({ field }) => (
<FormItem>
<FormLabel>Bot Token</FormLabel>
<FormLabel>{t("botToken")}</FormLabel>
<FormControl>
<Input placeholder="Telegram Bot Token" {...field} type="password" />
<Input placeholder={t("botTokenPlaceholder")} {...field} type="password" />
</FormControl>
<FormDescription>
Your Telegram bot token from @BotFather
{t("botTokenDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -419,38 +420,38 @@ export const NotificationChannelDialog = ({
/>
</>
)}
{notificationType === "discord" && (
<FormField
control={form.control}
name="discord_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormLabel>{t("discordWebhookUrl")}</FormLabel>
<FormControl>
<Input placeholder="https://discord.com/api/webhooks/..." {...field} />
<Input placeholder={t("discordWebhookUrlPlaceholder")} {...field} />
</FormControl>
<FormDescription>
Discord webhook URL from your server settings
{t("discordWebhookUrlDesc")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "slack" && (
<FormField
control={form.control}
name="slack_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormLabel>{t("slackWebhookUrl")}</FormLabel>
<FormControl>
<Input placeholder="https://hooks.slack.com/services/..." {...field} />
<Input placeholder={t("slackWebhookUrlPlaceholder")} {...field} />
</FormControl>
<FormDescription>
Slack incoming webhook URL
{t("slackWebhookUrlDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -465,12 +466,12 @@ export const NotificationChannelDialog = ({
name="signal_number"
render={({ field }) => (
<FormItem>
<FormLabel>Signal Number</FormLabel>
<FormLabel>{t("signalNumber")}</FormLabel>
<FormControl>
<Input placeholder="+1234567890" {...field} />
<Input placeholder={t("signalNumberPlaceholder")} {...field} />
</FormControl>
<FormDescription>
Signal phone number to send notifications to
{t("signalNumberDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -481,12 +482,12 @@ export const NotificationChannelDialog = ({
name="signal_api_endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Signal API Endpoint</FormLabel>
<FormLabel>{t("signalApiEndpoint")}</FormLabel>
<FormControl>
<Input placeholder="https://your-signal-api.com/v2/send" {...field} />
<Input placeholder={t("signalApiEndpointPlaceholder")} {...field} />
</FormControl>
<FormDescription>
The Rest API endpoint for your Signal service
{t("signalApiEndpointDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -501,12 +502,12 @@ export const NotificationChannelDialog = ({
name="google_chat_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Google Chat Webhook URL</FormLabel>
<FormLabel>{t("googleChatWebhookUrl")}</FormLabel>
<FormControl>
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
<Input placeholder={t("googleChatWebhookUrlPlaceholder")} {...field} />
</FormControl>
<FormDescription>
Google Chat webhook URL from your Google Chat space
{t("googleChatWebhookUrlDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -521,12 +522,12 @@ export const NotificationChannelDialog = ({
name="email_address"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormLabel>{t("emailAddress")}</FormLabel>
<FormControl>
<Input placeholder="notifications@example.com" {...field} type="email" />
<Input placeholder={t("emailAddressPlaceholder")} {...field} type="email" />
</FormControl>
<FormDescription>
Email address to send notifications to
{t("emailAddressDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -537,12 +538,12 @@ export const NotificationChannelDialog = ({
name="email_sender_name"
render={({ field }) => (
<FormItem>
<FormLabel>Sender Name</FormLabel>
<FormLabel>{t("emailSenderName")}</FormLabel>
<FormControl>
<Input placeholder="Alert System" {...field} />
<Input placeholder={t("emailSenderNamePlaceholder")} {...field} />
</FormControl>
<FormDescription>
Display name for outgoing emails
{t("emailSenderNameDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -554,9 +555,9 @@ export const NotificationChannelDialog = ({
name="smtp_server"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Server</FormLabel>
<FormLabel>{t("smtpServer")}</FormLabel>
<FormControl>
<Input placeholder="smtp.gmail.com" {...field} />
<Input placeholder={t("smtpServerPlaceholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -567,9 +568,9 @@ export const NotificationChannelDialog = ({
name="smtp_port"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Port</FormLabel>
<FormLabel>{t("smtpPort")}</FormLabel>
<FormControl>
<Input placeholder="587" {...field} />
<Input placeholder={t("smtpPortPlaceholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -581,12 +582,12 @@ export const NotificationChannelDialog = ({
name="smtp_password"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Password</FormLabel>
<FormLabel>{t("smtpPassword")}</FormLabel>
<FormControl>
<Input placeholder="Enter your SMTP password" {...field} type="password" />
<Input placeholder={t("smtpPasswordPlaceholder")} {...field} type="password" />
</FormControl>
<FormDescription>
Password for authenticating with the SMTP server
{t("smtpPasswordDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -602,12 +603,12 @@ export const NotificationChannelDialog = ({
name="ntfy_endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>NTFY Endpoint</FormLabel>
<FormLabel>{t("ntfyEndpoint")}</FormLabel>
<FormControl>
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
<Input placeholder={t("ntfyEndpointPlaceholder")} {...field} />
</FormControl>
<FormDescription>
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
{t("ntfyEndpointDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -618,12 +619,12 @@ export const NotificationChannelDialog = ({
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token (Optional)</FormLabel>
<FormLabel>{t("apiTokenOptional")}</FormLabel>
<FormControl>
<Input placeholder="Enter NTFY API token" {...field} type="password" />
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
</FormControl>
<FormDescription>
Optional API token for authentication with NTFY server
{t("apiTokenDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -632,19 +633,19 @@ export const NotificationChannelDialog = ({
</>
)}
{notificationType === "pushover" && (
{notificationType === "pushover" && (
<>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormLabel>{t("apiToken")}</FormLabel>
<FormControl>
<Input placeholder="Your Pushover API token" {...field} type="password" />
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
</FormControl>
<FormDescription>
Your Pushover application API token
{t("apiTokenDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -655,12 +656,12 @@ export const NotificationChannelDialog = ({
name="user_key"
render={({ field }) => (
<FormItem>
<FormLabel>User Key</FormLabel>
<FormLabel>{t("pushoverUserKey")}</FormLabel>
<FormControl>
<Input placeholder="Your Pushover user key" {...field} />
<Input placeholder={t("pushoverUserKeyPlaceholder")} {...field} />
</FormControl>
<FormDescription>
Your Pushover user key (or group key)
{t("pushoverUserKeyDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -676,12 +677,12 @@ export const NotificationChannelDialog = ({
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormLabel>{t("apiToken")}</FormLabel>
<FormControl>
<Input placeholder="Your Notifiarr API token" {...field} type="password" />
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
</FormControl>
<FormDescription>
Your Notifiarr API token for sending notifications
{t("apiTokenDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -692,12 +693,12 @@ export const NotificationChannelDialog = ({
name="channel_id"
render={({ field }) => (
<FormItem>
<FormLabel>Channel ID</FormLabel>
<FormLabel>{t("notifiarrChannelId")}</FormLabel>
<FormControl>
<Input placeholder="Discord Channel ID" {...field} />
<Input placeholder={t("notifiarrChannelIdPlaceholder")} {...field} />
</FormControl>
<FormDescription>
The Discord channel ID where notifications will be sent
{t("notifiarrChannelIdDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -713,12 +714,12 @@ export const NotificationChannelDialog = ({
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormLabel>{t("apiToken")}</FormLabel>
<FormControl>
<Input placeholder="Your Gotify API token" {...field} type="password" />
<Input placeholder={t("apiTokenPlaceholder")} {...field} type="password" />
</FormControl>
<FormDescription>
Your Gotify application API token
{t("apiTokenDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -729,12 +730,12 @@ export const NotificationChannelDialog = ({
name="server_url"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormLabel>{t("gotifyServerUrl")}</FormLabel>
<FormControl>
<Input placeholder="https://your-gotify-server.com" {...field} />
<Input placeholder={t("gotifyServerUrlPlaceholder")} {...field} />
</FormControl>
<FormDescription>
The URL of your Gotify server
{t("gotifyServerUrlDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -742,7 +743,7 @@ export const NotificationChannelDialog = ({
/>
</>
)}
{notificationType === "webhook" && (
<>
<FormField
@@ -750,12 +751,12 @@ export const NotificationChannelDialog = ({
name="webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormLabel>{t("webhookUrl")}</FormLabel>
<FormControl>
<Input placeholder="https://api.example.com/webhook" {...field} />
<Input placeholder={t("webhookUrlPlaceholder")} {...field} />
</FormControl>
<FormDescription>
The URL where webhook notifications will be sent
{t("webhookUrlDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -768,10 +769,9 @@ export const NotificationChannelDialog = ({
name="webhook_payload_template"
render={({ field }) => (
<FormItem>
<FormLabel>Payload Template (Optional)</FormLabel>
<FormLabel>{t("payloadTemplate")}</FormLabel>
<FormDescription>
JSON template for the webhook payload. Leave empty to use default template.
{t("payloadTemplateDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -780,15 +780,15 @@ export const NotificationChannelDialog = ({
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Payload Templates</CardTitle>
<CardTitle className="text-sm font-medium">{t("payloadTemplates")}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="mt-4">
<h4 className="text-sm font-medium mb-2">Available Placeholders:</h4>
<h4 className="text-sm font-medium mb-2">{t("availablePlaceholders")}</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Server:</p>
<p className="font-medium text-muted-foreground">{t("server")}</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{server_name}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{cpu_usage}'}</code><br/>
@@ -797,7 +797,7 @@ export const NotificationChannelDialog = ({
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Service:</p>
<p className="font-medium text-muted-foreground">{t("service")}</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{service_name}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{response_time}'}</code><br/>
@@ -806,7 +806,7 @@ export const NotificationChannelDialog = ({
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">SSL:</p>
<p className="font-medium text-muted-foreground">{t("ssl")}</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{domain}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{expiry_date}'}</code><br/>
@@ -815,7 +815,7 @@ export const NotificationChannelDialog = ({
</div>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground">Common:</p>
<p className="font-medium text-muted-foreground">{t("common")}</p>
<div className="space-y-0.5">
<code className="bg-muted px-1 rounded">${'{status}'}</code><br/>
<code className="bg-muted px-1 rounded">${'{time}'}</code><br/>
@@ -838,9 +838,9 @@ export const NotificationChannelDialog = ({
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Enabled</FormLabel>
<FormLabel>{t("enabled")}</FormLabel>
<FormDescription>
Enable or disable this notification channel
{t("enabledDesc")}
</FormDescription>
</div>
<FormControl>
@@ -855,11 +855,11 @@ export const NotificationChannelDialog = ({
<DialogFooter>
<Button variant="outline" type="button" onClick={handleClose}>
Cancel
{t("cancel")}
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? "Update Channel" : "Create Channel"}
{isEditing ? t("updateChannel") : t("createChannel")}
</Button>
</DialogFooter>
</form>
@@ -9,6 +9,7 @@ import { WebhookConfiguration, webhookService } from "@/services/webhookService"
import { NotificationChannelDialog } from "./NotificationChannelDialog";
import { NotificationChannelList } from "./NotificationChannelList";
import { pb } from "@/lib/pocketbase";
import { useLanguage } from "@/contexts/LanguageContext";
interface CombinedChannel extends Partial<AlertConfiguration> {
isWebhook?: boolean;
@@ -18,6 +19,7 @@ interface CombinedChannel extends Partial<AlertConfiguration> {
}
const NotificationSettings = () => {
const { t } = useLanguage();
const [isLoading, setIsLoading] = useState(true);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [webhookConfigs, setWebhookConfigs] = useState<WebhookConfiguration[]>([]);
@@ -126,13 +128,13 @@ const NotificationSettings = () => {
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Notification Settings</CardTitle>
<CardTitle>{t("titleNotification")}</CardTitle>
<CardDescription>
Configure notification channels for your services
{t("descriptionChannelsServices")}
</CardDescription>
</div>
<Button onClick={handleAddNew}>
<Plus className="mr-2 h-4 w-4" /> Add Channel
<Plus className="mr-2 h-4 w-4" /> {t("addChannel")}
</Button>
</div>
</CardHeader>
@@ -144,14 +146,14 @@ const NotificationSettings = () => {
className="w-full"
>
<TabsList className="mb-4">
<TabsTrigger value="all">All Channels</TabsTrigger>
<TabsTrigger value="telegram">Telegram</TabsTrigger>
<TabsTrigger value="discord">Discord</TabsTrigger>
<TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="google_chat">Google Chat</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
<TabsTrigger value="all">{t("all")}</TabsTrigger>
<TabsTrigger value="telegram">{t("telegram")}</TabsTrigger>
<TabsTrigger value="discord">{t("discord")}</TabsTrigger>
<TabsTrigger value="slack">{t("slack")}</TabsTrigger>
<TabsTrigger value="signal">{t("signal")}</TabsTrigger>
<TabsTrigger value="google_chat">{t("googleChat")}</TabsTrigger>
<TabsTrigger value="email">{t("email")}</TabsTrigger>
<TabsTrigger value="webhook">{t("webhook")}</TabsTrigger>
</TabsList>
<TabsContent value={currentTab} className="mt-0">