Include full contents of all nested repositories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
'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're looking for doesn'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user