Files
LetsBeBiz-Redesign/letsbe-hub/src/app/admin/enterprise-clients/[id]/servers/[serverId]/page.tsx

350 lines
12 KiB
TypeScript
Raw Normal View History

'use client'
import { useState, useCallback } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import {
ArrowLeft,
Server,
Power,
RefreshCw,
AlertTriangle,
Cpu,
HardDrive,
Network,
MemoryStick,
Activity,
Loader2,
Box,
Unlink,
Settings
} from 'lucide-react'
import {
useEnterpriseClient,
useClientServer,
useServerStatsHistory,
useCollectServerStats,
useServerAction,
useRemoveServerFromClient
} from '@/hooks/use-enterprise-clients'
import {
RangeSelector,
StatsCard,
CpuUsageChart,
MemoryUsageChart,
DiskIOChart,
NetworkChart
} from '@/components/admin/enterprise-stats-charts'
import { LiveStatsPanel } from '@/components/admin/live-stats-panel'
import { EnterpriseContainerList } from '@/components/admin/enterprise-container-list'
import type { StatsRange } from '@/lib/api/admin'
import type { StatsDataPoint } from '@/lib/services/stats-collection-service'
export default function ServerDetailPage() {
const params = useParams()
const router = useRouter()
const clientId = params.id as string
const serverId = params.serverId as string
const [range, setRange] = useState<StatsRange>('24h')
const [actionLoading, setActionLoading] = useState<string | null>(null)
const { data: client, isLoading: clientLoading } = useEnterpriseClient(clientId)
const { data: server, isLoading: serverLoading, refetch: refetchServer } = useClientServer(clientId, serverId)
const { data: statsData, isLoading: statsLoading } = useServerStatsHistory(clientId, serverId, range)
const collectStats = useCollectServerStats()
const serverAction = useServerAction()
const removeServer = useRemoveServerFromClient()
// Stable callback for refreshing stats (used by LiveStatsPanel)
const handleRefreshStats = useCallback(() => {
collectStats.mutate({ clientId, serverId })
}, [collectStats, clientId, serverId])
const isLoading = clientLoading || serverLoading
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
</div>
)
}
if (!client || !server) {
return (
<div className="text-center py-12">
<h2 className="text-lg font-medium text-gray-900">Server not found</h2>
<p className="mt-1 text-sm text-gray-500">
The server you&apos;re looking for doesn&apos;t exist or has been removed.
</p>
<Link
href={`/admin/enterprise-clients/${clientId}`}
className="mt-4 inline-flex items-center text-blue-600 hover:underline"
>
<ArrowLeft className="h-4 w-4 mr-1" />
Back to client
</Link>
</div>
)
}
// Convert API response to StatsDataPoint[] format for charts
const history: StatsDataPoint[] = statsData?.history?.map(h => ({
...h,
timestamp: new Date(h.timestamp)
})) || []
const latest = statsData?.latest
const handlePowerAction = async (command: 'ON' | 'OFF' | 'POWERCYCLE' | 'RESET') => {
setActionLoading(command)
try {
await serverAction.mutateAsync({
clientId,
serverId,
action: { action: 'power', command }
})
// Refresh server data after action
setTimeout(() => refetchServer(), 2000)
} catch (error) {
console.error('Power action failed:', error)
} finally {
setActionLoading(null)
}
}
const handleUnlinkServer = async () => {
if (!confirm(`Are you sure you want to unlink "${server?.nickname || server?.netcupServerId}" from this client?\n\nThis will remove the server from this enterprise client. The server itself will not be affected.`)) {
return
}
try {
await removeServer.mutateAsync({ clientId, serverId })
router.push(`/admin/enterprise-clients/${clientId}`)
} catch (error) {
console.error('Failed to unlink server:', error)
}
}
const statusColor = server.netcupStatus === 'RUNNING' || server.netcupStatus === 'running'
? 'bg-green-100 text-green-800'
: server.netcupStatus === 'SHUTOFF' || server.netcupStatus === 'stopped'
? 'bg-red-100 text-red-800'
: 'bg-yellow-100 text-yellow-800'
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/${clientId}`}
className="text-gray-500 hover:text-gray-700"
>
<ArrowLeft className="h-5 w-5" />
</Link>
<div>
<div className="flex items-center gap-2">
<Server className="h-5 w-5 text-gray-400" />
<h1 className="text-2xl font-bold text-gray-900">
{server.nickname || server.netcupServerId}
</h1>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}`}>
{server.netcupStatus}
</span>
</div>
<p className="text-sm text-gray-500 mt-1">
{server.purpose && `${server.purpose}`}
{client.name} Netcup ID: {server.netcupServerId}
</p>
</div>
</div>
{/* Power Controls */}
<div className="flex items-center gap-2">
<button
onClick={() => handlePowerAction('ON')}
disabled={actionLoading !== null}
className="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
{actionLoading === 'ON' ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Power className="h-4 w-4 mr-1 text-green-600" />
)}
Start
</button>
<button
onClick={() => handlePowerAction('OFF')}
disabled={actionLoading !== null}
className="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
{actionLoading === 'OFF' ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Power className="h-4 w-4 mr-1 text-red-600" />
)}
Stop
</button>
<button
onClick={() => handlePowerAction('POWERCYCLE')}
disabled={actionLoading !== null}
className="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
{actionLoading === 'POWERCYCLE' ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<RefreshCw className="h-4 w-4 mr-1" />
)}
Restart
</button>
<button
onClick={() => router.push(`/admin/enterprise-clients/${clientId}/servers/${serverId}/settings`)}
className="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<Settings className="h-4 w-4 mr-1" />
Settings
</button>
<button
onClick={() => router.push(`/admin/enterprise-clients/${clientId}/servers/${serverId}/danger`)}
className="inline-flex items-center px-3 py-1.5 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50"
>
<AlertTriangle className="h-4 w-4 mr-1" />
Danger Zone
</button>
<button
onClick={handleUnlinkServer}
disabled={removeServer.isPending}
className="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
{removeServer.isPending ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Unlink className="h-4 w-4 mr-1" />
)}
Unlink
</button>
</div>
</div>
{/* Server Info */}
{server.netcupIps && server.netcupIps.length > 0 && (
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center gap-6 text-sm">
<div>
<span className="text-gray-500">IP Address:</span>{' '}
<code className="bg-gray-200 px-2 py-0.5 rounded text-gray-800">
{server.netcupIps[0]}
</code>
</div>
{server.netcupHostname && (
<div>
<span className="text-gray-500">Hostname:</span>{' '}
<span className="font-medium">{server.netcupHostname}</span>
</div>
)}
</div>
</div>
)}
{/* Live Stats Panel with Auto-Refresh */}
<LiveStatsPanel
data={latest}
isRefreshing={collectStats.isPending}
onRefresh={handleRefreshStats}
autoRefreshInterval={15000}
/>
{/* Historical Charts */}
<div className="bg-white rounded-lg border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-medium text-gray-900">Historical Metrics</h2>
<RangeSelector value={range} onChange={setRange} />
</div>
{statsLoading ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
</div>
) : (
<div className="space-y-8">
{/* CPU Chart */}
<div>
<div className="flex items-center gap-2 mb-2">
<Cpu className="h-4 w-4 text-blue-500" />
<h3 className="text-sm font-medium text-gray-700">CPU Usage</h3>
</div>
<CpuUsageChart data={history} height={200} />
</div>
{/* Memory Chart */}
<div>
<div className="flex items-center gap-2 mb-2">
<MemoryStick className="h-4 w-4 text-green-500" />
<h3 className="text-sm font-medium text-gray-700">Memory Usage</h3>
</div>
<MemoryUsageChart data={history} height={200} />
</div>
{/* Disk I/O Chart */}
<div>
<div className="flex items-center gap-2 mb-2">
<HardDrive className="h-4 w-4 text-purple-500" />
<h3 className="text-sm font-medium text-gray-700">Disk I/O</h3>
</div>
<DiskIOChart data={history} height={200} />
</div>
{/* Network Chart */}
<div>
<div className="flex items-center gap-2 mb-2">
<Network className="h-4 w-4 text-cyan-500" />
<h3 className="text-sm font-medium text-gray-700">Network Traffic</h3>
</div>
<NetworkChart data={history} height={200} />
</div>
</div>
)}
</div>
{/* Quick Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatsCard
title="CPU Average"
value={latest?.cpuPercent?.toFixed(1) ?? null}
unit="%"
icon={<Cpu className="h-5 w-5" />}
/>
<StatsCard
title="Memory Used"
value={latest?.memoryUsedMb?.toFixed(0) ?? null}
unit="MB"
icon={<MemoryStick className="h-5 w-5" />}
/>
<StatsCard
title="Disk Read"
value={latest?.diskReadMbps?.toFixed(2) ?? null}
unit="MB/s"
icon={<HardDrive className="h-5 w-5" />}
/>
<StatsCard
title="Network In"
value={latest?.networkInMbps?.toFixed(2) ?? null}
unit="Mbps"
icon={<Activity className="h-5 w-5" />}
/>
</div>
{/* Containers */}
<div className="bg-white rounded-lg border border-gray-200 p-6">
<div className="flex items-center gap-2 mb-4">
<Box className="h-5 w-5 text-gray-400" />
<h2 className="text-lg font-medium text-gray-900">Containers</h2>
</div>
<EnterpriseContainerList clientId={clientId} serverId={serverId} />
</div>
</div>
)
}