letsbe-hub/src/app/admin/enterprise-clients/[id]/page.tsx

909 lines
31 KiB
TypeScript

'use client'
import { use, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
useEnterpriseClient,
useUpdateEnterpriseClient,
useDeleteEnterpriseClient,
useClientServers,
useServerAction,
useErrorRules,
useCreateErrorRule,
useDetectedErrors,
useAcknowledgeError,
useAddServerToClient,
} from '@/hooks/use-enterprise-clients'
import { useNetcupServers } from '@/hooks/use-netcup'
import {
ArrowLeft,
Building2,
Server,
AlertTriangle,
Mail,
Phone,
FileText,
Edit,
Trash2,
Power,
RotateCcw,
Plus,
Check,
X,
Loader2,
AlertCircle,
RefreshCw,
Activity,
HardDrive,
Cpu,
MemoryStick,
Clock,
ShieldAlert,
Eye,
} from 'lucide-react'
import type { EnterpriseServerWithStatus, ErrorDetectionRule, DetectedError, ErrorSeverity } from '@/types/api'
// Overview stat card
function OverviewCard({
title,
value,
icon: Icon,
className = ''
}: {
title: string
value: string | number
icon: typeof Cpu
className?: string
}) {
return (
<Card className={className}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-muted">
<Icon className="h-4 w-4 text-muted-foreground" />
</div>
<div>
<p className="text-xs text-muted-foreground">{title}</p>
<p className="text-lg font-semibold">{value}</p>
</div>
</div>
</CardContent>
</Card>
)
}
// Server card component
function ServerCard({
server,
clientId,
onPowerAction
}: {
server: EnterpriseServerWithStatus
clientId: string
onPowerAction: (serverId: string, command: 'ON' | 'OFF' | 'POWERCYCLE') => void
}) {
const statusColors: Record<string, { bg: string; text: string; dot: string }> = {
running: { bg: 'bg-emerald-50 dark:bg-emerald-950/30', text: 'text-emerald-700 dark:text-emerald-400', dot: 'bg-emerald-500 animate-pulse' },
stopped: { bg: 'bg-slate-50 dark:bg-slate-950/30', text: 'text-slate-600 dark:text-slate-400', dot: 'bg-slate-400' },
error: { bg: 'bg-red-50 dark:bg-red-950/30', text: 'text-red-700 dark:text-red-400', dot: 'bg-red-500' },
unknown: { bg: 'bg-amber-50 dark:bg-amber-950/30', text: 'text-amber-700 dark:text-amber-400', dot: 'bg-amber-500' }
}
const status = server.netcupStatus?.toLowerCase() || 'unknown'
const colors = statusColors[status] || statusColors.unknown
return (
<Card className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-muted">
<Server className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<Link
href={`/admin/enterprise-clients/${clientId}/servers/${server.id}`}
className="font-medium hover:text-primary hover:underline"
>
{server.nickname || server.netcupServerId}
</Link>
{server.purpose && (
<p className="text-xs text-muted-foreground">{server.purpose}</p>
)}
<div className="flex items-center gap-2 mt-1">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${colors.bg} ${colors.text}`}>
<span className={`h-1.5 w-1.5 rounded-full ${colors.dot}`} />
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
</div>
{server.netcupIps?.length > 0 && (
<p className="text-xs text-muted-foreground mt-1 font-mono">
{server.netcupIps[0]}
</p>
)}
</div>
</div>
<div className="flex items-center gap-1">
<Link href={`/admin/enterprise-clients/${clientId}/servers/${server.id}`}>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
title="View Server"
>
<Eye className="h-4 w-4" />
</Button>
</Link>
{status === 'running' ? (
<>
<Button
variant="ghost"
size="sm"
onClick={() => onPowerAction(server.id, 'POWERCYCLE')}
className="h-8 w-8 p-0"
title="Restart"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onPowerAction(server.id, 'OFF')}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
title="Power Off"
>
<Power className="h-4 w-4" />
</Button>
</>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => onPowerAction(server.id, 'ON')}
className="h-8 w-8 p-0 text-emerald-600"
title="Power On"
>
<Power className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
)
}
// Error rule row
function ErrorRuleRow({ rule }: { rule: ErrorDetectionRule }) {
const severityColors: Record<string, string> = {
INFO: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
WARNING: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
ERROR: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
CRITICAL: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
}
return (
<div className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded ${rule.isActive ? 'bg-emerald-100 dark:bg-emerald-900/30' : 'bg-slate-100 dark:bg-slate-900/30'}`}>
{rule.isActive ? (
<Check className="h-3.5 w-3.5 text-emerald-600" />
) : (
<X className="h-3.5 w-3.5 text-slate-400" />
)}
</div>
<div>
<p className="font-medium text-sm">{rule.name}</p>
<p className="text-xs text-muted-foreground font-mono truncate max-w-xs">
{rule.pattern}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${severityColors[rule.severity]}`}>
{rule.severity}
</span>
<span className="text-xs text-muted-foreground">
{rule._count?.detectedErrors || 0} matches
</span>
</div>
</div>
)
}
// Detected error row
function DetectedErrorRow({
error,
onAcknowledge
}: {
error: DetectedError
onAcknowledge: () => void
}) {
const severityColors: Record<string, string> = {
INFO: 'border-l-blue-500',
WARNING: 'border-l-amber-500',
ERROR: 'border-l-red-500',
CRITICAL: 'border-l-purple-500'
}
const isAcknowledged = !!error.acknowledgedAt
return (
<div className={`p-3 border rounded-lg border-l-4 ${severityColors[error.rule?.severity || 'INFO']} ${isAcknowledged ? 'opacity-60' : ''}`}>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{error.rule?.name}</span>
{error.containerName && (
<span className="text-xs text-muted-foreground">
in {error.containerName}
</span>
)}
</div>
<p className="text-xs text-muted-foreground mt-1 font-mono truncate">
{error.logLine}
</p>
<p className="text-xs text-muted-foreground mt-1">
{new Date(error.timestamp).toLocaleString()}
{error.server && `${error.server.nickname || error.server.netcupServerId}`}
</p>
</div>
{!isAcknowledged && (
<Button
variant="ghost"
size="sm"
onClick={onAcknowledge}
className="h-8 px-2 text-xs"
>
<Check className="h-3.5 w-3.5 mr-1" />
Ack
</Button>
)}
</div>
</div>
)
}
// Add rule dialog
function AddRuleDialog({
open,
onOpenChange,
clientId
}: {
open: boolean
onOpenChange: (open: boolean) => void
clientId: string
}) {
const [formData, setFormData] = useState({
name: '',
pattern: '',
severity: 'WARNING' as ErrorSeverity,
description: ''
})
const createRule = useCreateErrorRule()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await createRule.mutateAsync({
clientId,
data: {
name: formData.name,
pattern: formData.pattern,
severity: formData.severity,
description: formData.description || undefined
}
})
setFormData({ name: '', pattern: '', severity: 'WARNING', description: '' })
onOpenChange(false)
} catch {
// Error handled by mutation
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add Error Detection Rule</DialogTitle>
<DialogDescription>
Create a regex pattern to detect errors in container logs.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="ruleName">Rule Name *</Label>
<Input
id="ruleName"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Database Connection Failed"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="pattern">Regex Pattern *</Label>
<Input
id="pattern"
value={formData.pattern}
onChange={(e) => setFormData({ ...formData, pattern: e.target.value })}
placeholder="error|ERROR|failed|FAILED"
className="font-mono text-sm"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="severity">Severity</Label>
<Select
value={formData.severity}
onValueChange={(value) => setFormData({ ...formData, severity: value as ErrorSeverity })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="INFO">Info</SelectItem>
<SelectItem value="WARNING">Warning</SelectItem>
<SelectItem value="ERROR">Error</SelectItem>
<SelectItem value="CRITICAL">Critical</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Detects database connection errors"
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={createRule.isPending}>
{createRule.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Add Rule
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
// Add server dialog
function AddServerDialog({
open,
onOpenChange,
clientId,
existingServerIds
}: {
open: boolean
onOpenChange: (open: boolean) => void
clientId: string
existingServerIds: string[]
}) {
const [formData, setFormData] = useState({
netcupServerId: '',
nickname: '',
purpose: '',
portainerUrl: '',
portainerUsername: '',
portainerPassword: ''
})
const { data: netcupServers, isLoading: loadingServers } = useNetcupServers(false)
const addServer = useAddServerToClient()
// Filter out servers that are already linked
const availableServers = netcupServers?.servers?.filter(
(s: { id: string }) => !existingServerIds.includes(s.id)
) || []
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.netcupServerId) return
try {
await addServer.mutateAsync({
clientId,
data: {
netcupServerId: formData.netcupServerId,
nickname: formData.nickname || undefined,
purpose: formData.purpose || undefined,
portainerUrl: formData.portainerUrl || undefined,
portainerUsername: formData.portainerUsername || undefined,
portainerPassword: formData.portainerPassword || undefined
}
})
setFormData({
netcupServerId: '',
nickname: '',
purpose: '',
portainerUrl: '',
portainerUsername: '',
portainerPassword: ''
})
onOpenChange(false)
} catch {
// Error handled by mutation
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Add Server to Client</DialogTitle>
<DialogDescription>
Link a Netcup server to this enterprise client.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="netcupServer">Netcup Server *</Label>
{loadingServers ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground p-2 border rounded-md">
<Loader2 className="h-4 w-4 animate-spin" />
Loading servers...
</div>
) : availableServers.length === 0 ? (
<div className="text-sm text-muted-foreground p-2 border rounded-md">
No available servers. All Netcup servers are already linked.
</div>
) : (
<Select
value={formData.netcupServerId}
onValueChange={(value) => setFormData({ ...formData, netcupServerId: value })}
>
<SelectTrigger>
<SelectValue placeholder="Select a server..." />
</SelectTrigger>
<SelectContent>
{availableServers.map((server: { id: string; name: string; nickname?: string; hostname?: string; ips?: string[] }) => (
<SelectItem key={server.id} value={server.id}>
<div className="flex flex-col">
<span className="font-medium">{server.nickname || server.name}</span>
<span className="text-xs text-muted-foreground">
{server.ips?.[0] || 'No IP'}
{server.hostname && `${server.hostname}`}
{server.nickname && `${server.name}`}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="nickname">Nickname</Label>
<Input
id="nickname"
value={formData.nickname}
onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
placeholder="Production Server"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="purpose">Purpose</Label>
<Input
id="purpose"
value={formData.purpose}
onChange={(e) => setFormData({ ...formData, purpose: e.target.value })}
placeholder="Web hosting"
/>
</div>
</div>
<div className="border-t pt-4 mt-2">
<p className="text-sm font-medium mb-3">Portainer Credentials (Optional)</p>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="portainerUrl">Portainer URL</Label>
<Input
id="portainerUrl"
value={formData.portainerUrl}
onChange={(e) => setFormData({ ...formData, portainerUrl: e.target.value })}
placeholder="https://portainer.example.com"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="portainerUsername">Username</Label>
<Input
id="portainerUsername"
value={formData.portainerUsername}
onChange={(e) => setFormData({ ...formData, portainerUsername: e.target.value })}
placeholder="admin"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="portainerPassword">Password</Label>
<Input
id="portainerPassword"
type="password"
value={formData.portainerPassword}
onChange={(e) => setFormData({ ...formData, portainerPassword: e.target.value })}
placeholder="••••••••"
/>
</div>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={addServer.isPending || !formData.netcupServerId}>
{addServer.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Add Server
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export default function EnterpriseClientDetailPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id: clientId } = use(params)
const router = useRouter()
const [showAddRuleDialog, setShowAddRuleDialog] = useState(false)
const [showAddServerDialog, setShowAddServerDialog] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const { data: client, isLoading, isError, error, refetch } = useEnterpriseClient(clientId)
const { data: servers } = useClientServers(clientId)
const { data: errorRules } = useErrorRules(clientId)
const { data: detectedErrors } = useDetectedErrors(clientId, { acknowledged: false, limit: 50 })
const deleteClient = useDeleteEnterpriseClient()
const serverAction = useServerAction()
const acknowledgeError = useAcknowledgeError()
const handlePowerAction = async (serverId: string, command: 'ON' | 'OFF' | 'POWERCYCLE') => {
try {
await serverAction.mutateAsync({
clientId,
serverId,
action: { action: 'power', command }
})
} catch {
// Error handled by mutation
}
}
const handleAcknowledgeError = async (errorId: string) => {
try {
await acknowledgeError.mutateAsync({ clientId, errorId })
} catch {
// Error handled by mutation
}
}
const handleDeleteClient = async () => {
try {
await deleteClient.mutateAsync(clientId)
router.push('/admin/enterprise-clients')
} catch {
// Error handled by mutation
}
}
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center h-[50vh] gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Loading client details...</p>
</div>
)
}
if (isError || !client) {
return (
<div className="flex flex-col items-center justify-center h-[50vh] gap-4">
<AlertCircle className="h-8 w-8 text-destructive" />
<p className="font-medium text-destructive">Failed to load client</p>
<p className="text-sm text-muted-foreground">
{error instanceof Error ? error.message : 'Client not found'}
</p>
<Button variant="outline" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/admin/enterprise-clients">
<Button variant="ghost" size="sm" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-gradient-to-br from-primary/10 to-primary/20">
<Building2 className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-2xl font-bold">{client.name}</h1>
{client.companyName && (
<p className="text-muted-foreground">{client.companyName}</p>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="gap-2">
<Edit className="h-4 w-4" />
Edit
</Button>
<Button
variant="outline"
size="sm"
className="gap-2 text-destructive hover:text-destructive"
onClick={() => setShowDeleteDialog(true)}
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div>
</div>
{/* Client info cards */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Mail className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">Contact Email</p>
<p className="font-medium">{client.contactEmail}</p>
</div>
</div>
</CardContent>
</Card>
{client.contactPhone && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Phone className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">Contact Phone</p>
<p className="font-medium">{client.contactPhone}</p>
</div>
</div>
</CardContent>
</Card>
)}
{client.notes && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<FileText className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">Notes</p>
<p className="font-medium text-sm">{client.notes}</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
{/* Overview stats */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<OverviewCard
title="Servers"
value={client.statsOverview?.totalServers || 0}
icon={Server}
/>
<OverviewCard
title="Avg CPU"
value={client.statsOverview?.avgCpuPercent != null ? `${client.statsOverview.avgCpuPercent}%` : '-'}
icon={Cpu}
/>
<OverviewCard
title="Containers"
value={`${client.statsOverview?.runningContainers || 0}/${client.statsOverview?.totalContainers || 0}`}
icon={HardDrive}
/>
<OverviewCard
title="Open Errors"
value={client.statsOverview?.unacknowledgedErrors || 0}
icon={AlertTriangle}
className={client.statsOverview?.unacknowledgedErrors ? 'border-red-200 dark:border-red-900' : ''}
/>
</div>
{/* Tabs */}
<Tabs defaultValue="servers" className="space-y-4">
<TabsList>
<TabsTrigger value="servers" className="gap-2">
<Server className="h-4 w-4" />
Servers ({servers?.length || 0})
</TabsTrigger>
<TabsTrigger value="errors" className="gap-2">
<AlertTriangle className="h-4 w-4" />
Errors ({detectedErrors?.length || 0})
</TabsTrigger>
<TabsTrigger value="rules" className="gap-2">
<ShieldAlert className="h-4 w-4" />
Rules ({errorRules?.length || 0})
</TabsTrigger>
</TabsList>
<TabsContent value="servers" className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{servers?.length || 0} server{servers?.length !== 1 ? 's' : ''} linked
</p>
<Button size="sm" className="gap-2" onClick={() => setShowAddServerDialog(true)}>
<Plus className="h-4 w-4" />
Add Server
</Button>
</div>
{servers && servers.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{servers.map((server) => (
<ServerCard
key={server.id}
server={server}
clientId={clientId}
onPowerAction={handlePowerAction}
/>
))}
</div>
) : (
<Card>
<CardContent className="py-12 text-center">
<Server className="h-12 w-12 text-muted-foreground/40 mx-auto mb-4" />
<p className="text-muted-foreground">No servers linked to this client yet.</p>
<Button className="mt-4 gap-2" onClick={() => setShowAddServerDialog(true)}>
<Plus className="h-4 w-4" />
Add Server
</Button>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="errors" className="space-y-4">
{detectedErrors && detectedErrors.length > 0 ? (
<div className="space-y-2">
{detectedErrors.map((err) => (
<DetectedErrorRow
key={err.id}
error={err}
onAcknowledge={() => handleAcknowledgeError(err.id)}
/>
))}
</div>
) : (
<Card>
<CardContent className="py-12 text-center">
<Check className="h-12 w-12 text-emerald-500/40 mx-auto mb-4" />
<p className="text-muted-foreground">No unacknowledged errors.</p>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="rules" className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Define regex patterns to detect errors in container logs.
</p>
<Button size="sm" className="gap-2" onClick={() => setShowAddRuleDialog(true)}>
<Plus className="h-4 w-4" />
Add Rule
</Button>
</div>
{errorRules && errorRules.length > 0 ? (
<div className="space-y-2">
{errorRules.map((rule) => (
<ErrorRuleRow key={rule.id} rule={rule} />
))}
</div>
) : (
<Card>
<CardContent className="py-12 text-center">
<ShieldAlert className="h-12 w-12 text-muted-foreground/40 mx-auto mb-4" />
<p className="text-muted-foreground">No error detection rules configured.</p>
<Button className="mt-4 gap-2" onClick={() => setShowAddRuleDialog(true)}>
<Plus className="h-4 w-4" />
Add Rule
</Button>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
{/* Add Rule Dialog */}
<AddRuleDialog
open={showAddRuleDialog}
onOpenChange={setShowAddRuleDialog}
clientId={clientId}
/>
{/* Add Server Dialog */}
<AddServerDialog
open={showAddServerDialog}
onOpenChange={setShowAddServerDialog}
clientId={clientId}
existingServerIds={servers?.map(s => s.netcupServerId) || []}
/>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Enterprise Client</DialogTitle>
<DialogDescription>
Are you sure you want to delete {client.name}? This will remove all associated servers, error rules, and detected errors. This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setShowDeleteDialog(false)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteClient}
disabled={deleteClient.isPending}
>
{deleteClient.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Delete Client
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}