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:
@@ -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 you’re 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 you’re 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>
|
||||
|
||||
+88
-88
@@ -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>
|
||||
|
||||
+13
-11
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user