import { Activity, AlertTriangle, RotateCcw, Server } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { Badge, Button, Card, CardHeader } from '../../components/ui'; import { PageTitle } from '../../layout/AppLayout'; import { getStoredSelectedAppId, setStoredSelectedAppId } from '../../lib/selected-app'; import { runtimeStatusDotClass } from '../../lib/status-dot'; import { cn } from '../../lib/utils'; import type { ApiApp, RuntimeEvent, RuntimePodMetric, RuntimeResource } from '../../types/runtime'; import { loadApps, loadRuntimeEvents, loadRuntimePodMetrics, loadRuntimeResources } from '../runtime/runtime-api'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; type ResourceRow = RuntimeResource & { metric?: RuntimePodMetric; }; const chartColors = ['#34d399', '#60a5fa', '#fbbf24', '#fb7185', '#a78bfa']; const commonAppMonitoringAppId = '__common_apps__'; const commonAppMonitoringApp: ApiApp = { id: commonAppMonitoringAppId, key: 'studio', name: '공통앱', namespace: 'studio-system', status: 'ACTIVE' }; const eventRanges = [ { label: '최근 10분', value: '10m', milliseconds: 10 * 60 * 1000 }, { label: '1시간', value: '1h', milliseconds: 60 * 60 * 1000 }, { label: '6시간', value: '6h', milliseconds: 6 * 60 * 60 * 1000 }, { label: '24시간', value: '24h', milliseconds: 24 * 60 * 60 * 1000 } ]; function parseCpuMillicores(value?: string) { if (!value) return 0; if (value.endsWith('m')) return Number(value.slice(0, -1)) || 0; return (Number(value) || 0) * 1000; } function parseMemoryMi(value?: string) { if (!value) return 0; const matched = value.match(/^([0-9.]+)([A-Za-z]+)?$/); if (!matched) return 0; const amount = Number(matched[1]); const unit = matched[2] ?? ''; if (!Number.isFinite(amount)) return 0; if (unit === 'Ki') return amount / 1024; if (unit === 'Gi') return amount * 1024; if (unit === 'Ti') return amount * 1024 * 1024; if (unit === 'Mi' || unit === 'M') return amount; return amount / 1024 / 1024; } function isHealthy(resource: RuntimeResource) { if (['Error', 'CrashLoopBackOff', 'Failed'].includes(resource.status)) return false; if (!resource.ready.includes('/')) return ['Running', 'Bound', 'ClusterIP', 'LoadBalancer'].includes(resource.status); const [ready, total] = resource.ready.split('/').map((value) => Number(value)); return Number.isFinite(ready) && Number.isFinite(total) && total > 0 && ready === total; } function statusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'blue' { if (isHealthy({ kind: '', name: '', status, ready: status === 'Running' ? '1/1' : '-', restarts: null, age: '', detail: '' })) return 'green'; if (['Pending', 'ContainerCreating'].includes(status)) return 'amber'; if (['Error', 'CrashLoopBackOff', 'Failed'].includes(status)) return 'red'; return 'neutral'; } function isCompletedMigration(resource: RuntimeResource) { const normalizedName = resource.name.toLowerCase(); const normalizedStatus = resource.status.toLowerCase(); return normalizedName.includes('migration') && ['completed', 'succeeded', 'success'].includes(normalizedStatus); } function eventTime(event: RuntimeEvent) { const value = event.lastTimestamp ?? event.firstTimestamp; if (!value) return 0; const time = new Date(value).getTime(); return Number.isFinite(time) ? time : 0; } export function MonitoringPage() { const [apps, setApps] = useState([]); const [selectedAppId, setSelectedAppId] = useState(() => getStoredSelectedAppId()); const [eventRange, setEventRange] = useState('1h'); const [resources, setResources] = useState([]); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const loadingVisible = useMinimumLoading(loading); const selectedApp = apps.find((app) => app.id === selectedAppId) ?? apps[0]; const selectedRange = eventRanges.find((range) => range.value === eventRange) ?? eventRanges[1]; const visibleResources = useMemo(() => resources.filter((resource) => !isCompletedMigration(resource)), [resources]); const filteredEvents = useMemo(() => { const threshold = Date.now() - selectedRange.milliseconds; return events.filter((event) => eventTime(event) >= threshold); }, [events, selectedRange.milliseconds]); const refresh = () => { setLoading(true); loadApps() .then(async (nextApps) => { const appItems = [commonAppMonitoringApp, ...nextApps.filter((app) => app.namespace)]; const storedAppId = getStoredSelectedAppId(); const preferredAppId = selectedAppId || storedAppId; const app = appItems.find((item) => item.id === preferredAppId) ?? appItems.find((item) => item.id === storedAppId) ?? appItems[0]; setApps(appItems); setSelectedAppId(app?.id ?? ''); if (!app?.namespace) { setResources([]); setEvents([]); return; } const [runtime, metrics, eventItems] = await Promise.all([ loadRuntimeResources(app.namespace), loadRuntimePodMetrics(app.namespace).catch(() => []), loadRuntimeEvents(app.namespace).catch(() => []) ]); const metricByPod = new Map(metrics.map((metric) => [metric.pod, metric])); setResources((runtime.resources ?? []).map((resource) => ({ ...resource, metric: resource.kind === 'Pod' ? metricByPod.get(resource.name) : undefined }))); setEvents(eventItems); }) .finally(() => setLoading(false)); }; useEffect(() => { refresh(); }, []); useEffect(() => { if (!selectedApp?.namespace) return; setLoading(true); Promise.all([ loadRuntimeResources(selectedApp.namespace), loadRuntimePodMetrics(selectedApp.namespace).catch(() => []), loadRuntimeEvents(selectedApp.namespace).catch(() => []) ]) .then(([runtime, metrics, eventItems]) => { const metricByPod = new Map(metrics.map((metric) => [metric.pod, metric])); setResources((runtime.resources ?? []).map((resource) => ({ ...resource, metric: resource.kind === 'Pod' ? metricByPod.get(resource.name) : undefined }))); setEvents(eventItems); }) .finally(() => setLoading(false)); }, [selectedAppId]); useEffect(() => { if (!selectedApp?.namespace) return; const namespace = selectedApp.namespace; const timer = window.setInterval(() => { Promise.all([ loadRuntimeResources(namespace), loadRuntimePodMetrics(namespace).catch(() => []), loadRuntimeEvents(namespace).catch(() => []) ]) .then(([runtime, metrics, eventItems]) => { const metricByPod = new Map(metrics.map((metric) => [metric.pod, metric])); setResources((runtime.resources ?? []).map((resource) => ({ ...resource, metric: resource.kind === 'Pod' ? metricByPod.get(resource.name) : undefined }))); setEvents(eventItems); }) .catch(() => undefined); }, 10000); return () => window.clearInterval(timer); }, [selectedApp?.namespace]); const summary = useMemo(() => { const runtimeResources = visibleResources.filter((resource) => ['Pod', 'Deployment', 'StatefulSet', 'DaemonSet'].includes(resource.kind)); const healthy = runtimeResources.filter(isHealthy).length; const warnings = filteredEvents.filter((event) => event.type === 'Warning').length; const restarts = visibleResources.reduce((sum, resource) => sum + (resource.restarts ?? 0), 0); return { total: runtimeResources.length, healthy, warnings, restarts }; }, [filteredEvents, visibleResources]); const podMetrics = useMemo(() => visibleResources .filter((resource) => resource.kind === 'Pod' && resource.metric) .map((resource) => ({ name: resource.name.replace(`${selectedApp?.key?.toLowerCase() ?? ''}-`, ''), cpu: parseCpuMillicores(resource.metric?.cpu), memory: Math.round(parseMemoryMi(resource.metric?.memory)) })), [visibleResources, selectedApp?.key]); const statusData = useMemo(() => { const byStatus = new Map(); visibleResources.forEach((resource) => byStatus.set(resource.status || '-', (byStatus.get(resource.status || '-') ?? 0) + 1)); return [...byStatus.entries()].map(([name, value]) => ({ name, value })); }, [visibleResources]); return ( <> 새로고침 } />

검색 기간

{eventRanges.map((range) => ( ))}
resource.kind === 'Pod').length)} hint="현재 기준" tone="blue" /> 0 ? 'amber' : 'green'} /> 0 ? 'amber' : 'green'} />
{statusData.map((entry, index) => )}
{statusData.map((entry, index) => (
{entry.name} {entry.value}
))}
{visibleResources.map((resource) => (

{resource.kind}/{resource.name}

{resource.ready || '-'} · 재시작 {resource.restarts ?? 0}

{resource.status || '-'}
))}
{filteredEvents.slice(0, 12).map((event, index) => (
{event.type ?? '-'}

{event.reason ?? '-'}

{event.message ?? '-'}

))} {!filteredEvents.length ?
선택 기간에 표시할 이벤트가 없습니다.
: null}
); } function MetricCard({ icon: Icon, label, value, hint, tone }: { icon: typeof Server; label: string; value: string; hint: string; tone: 'green' | 'amber' | 'red' | 'blue' | 'neutral' }) { return (

{label}

{value}

{tone === 'green' ? '정상' : tone === 'amber' ? '주의' : tone === 'red' ? '장애' : '정보'}

{hint}

); }