import { RotateCcw } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table'; import { Badge, Button, Card, CardHeader } from '../../components/ui'; import { PageTitle } from '../../layout/AppLayout'; import { runtimeStatusDotClass } from '../../lib/status-dot'; import { cn } from '../../lib/utils'; import type { RuntimePodMetric, RuntimeResource, RuntimeResourceResponse } from '../../types/runtime'; import { loadApps, loadRuntimeEvents, loadRuntimePodMetrics, loadRuntimeResources } from '../runtime/runtime-api'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; const defaultNamespaces = ['studio-system', 'stdo-lms', 'stdo-ads-ide']; type NamespaceRuntime = RuntimeResourceResponse & { failed?: boolean; }; type InfrastructureData = { namespaces: string[]; runtimes: NamespaceRuntime[]; metrics: RuntimePodMetric[]; }; type ResourceRow = RuntimeResource & { id: string; namespace: string; metric?: RuntimePodMetric; }; function uniqueNamespaces(values: Array) { return [...new Set(values.filter(Boolean) as string[])].sort((a, b) => a.localeCompare(b)); } function isWorkload(resource: RuntimeResource) { return ['Deployment', 'StatefulSet', 'DaemonSet', 'Pod', 'Job', 'CronJob', 'ReplicaSet'].includes(resource.kind); } function isNetwork(resource: RuntimeResource) { return ['Service', 'Ingress', 'Endpoint', 'Endpoints'].includes(resource.kind); } function isStorage(resource: RuntimeResource) { return ['PersistentVolumeClaim', 'PersistentVolume', 'StorageClass'].includes(resource.kind); } function resourceStatusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'blue' { const normalized = status.toLowerCase(); if (['running', 'ready', 'active', 'bound'].some((word) => normalized.includes(word))) return 'green'; if (['pending', 'progressing', 'containercreating'].some((word) => normalized.includes(word))) return 'amber'; if (['failed', 'error', 'crash', 'unhealthy', 'notready'].some((word) => normalized.includes(word))) return 'red'; if (['completed', 'succeeded'].some((word) => normalized.includes(word))) return 'neutral'; return 'blue'; } function metricFor(resource: ResourceRow, metrics: RuntimePodMetric[]) { if (resource.kind !== 'Pod') return undefined; return metrics.find((metric) => metric.namespace === resource.namespace && metric.pod === resource.name); } function useInfrastructureData() { const [data, setData] = useState({ namespaces: defaultNamespaces, runtimes: [], metrics: [] }); const [loading, setLoading] = useState(true); const [failed, setFailed] = useState(false); const loadingVisible = useMinimumLoading(loading); const refresh = useCallback(() => { setLoading(true); Promise.all([ loadApps().catch(() => []), loadRuntimePodMetrics().catch(() => [] as RuntimePodMetric[]) ]) .then(async ([apps, metrics]) => { const namespaces = uniqueNamespaces([...defaultNamespaces, ...apps.map((app) => app.namespace)]); const runtimes = await Promise.all( namespaces.map((namespace) => ( loadRuntimeResources(namespace) .then((runtime) => runtime) .catch(() => ({ namespace, exists: false, failed: true, summary: { pods: 0, deployments: 0, statefulSets: 0, services: 0, persistentVolumeClaims: 0 }, resources: [] })) )) ); setData({ namespaces, runtimes, metrics }); setFailed(false); }) .catch(() => { setFailed(true); }) .finally(() => setLoading(false)); }, []); useEffect(() => { refresh(); }, [refresh]); return { data, loading, loadingVisible, failed, refresh }; } function rowsFromData(data: InfrastructureData, predicate: (resource: RuntimeResource) => boolean) { return data.runtimes.flatMap((runtime) => ( runtime.resources .filter(predicate) .map((resource) => { const row = { ...resource, namespace: runtime.namespace, id: `${runtime.namespace}:${resource.kind}:${resource.name}` }; return { ...row, metric: metricFor(row, data.metrics) }; }) )); } function ResourceStatus({ status }: { status: string }) { return (
{status || '-'}
); } function NamespaceFilter(rows: ResourceRow[]): DataTableFilter { return { key: 'namespace', label: '네임스페이스', options: uniqueNamespaces(rows.map((row) => row.namespace)).map((namespace) => ({ label: namespace, value: namespace })), getValue: (row) => row.namespace }; } function KindFilter(rows: ResourceRow[]): DataTableFilter { return { key: 'kind', label: '종류', options: uniqueNamespaces(rows.map((row) => row.kind)).map((kind) => ({ label: kind, value: kind })), getValue: (row) => row.kind }; } function InfraPageShell({ title, description, children, loading, loadingVisible, failed, onRefresh }: { title: string; description: string; children: ReactNode; loading: boolean; loadingVisible: boolean; failed: boolean; onRefresh: () => void; }) { return ( <> 새로고침 } /> {failed ? (
인프라 데이터를 불러오지 못했습니다.
) : null} {children} ); } function ResourceTable({ title, description, rows, loading, emptyMessage }: { title: string; description: string; rows: ResourceRow[]; loading: boolean; emptyMessage: string; }) { const columns = useMemo>>( () => [ { key: 'namespace', header: '네임스페이스', className: 'w-44 px-5 py-3', cell: (row) => {row.namespace} }, { key: 'kind', header: '종류', className: 'w-40 px-5 py-3', cell: (row) => {row.kind} }, { key: 'name', header: '이름', className: 'min-w-[240px] px-5 py-3', cell: (row) => {row.name} }, { key: 'status', header: '상태', className: 'w-44 px-5 py-3', cell: (row) => }, { key: 'ready', header: 'Ready', className: 'w-28 px-5 py-3', cell: (row) => {row.ready || '-'} }, { key: 'restarts', header: '재시작', className: 'w-24 px-5 py-3', cell: (row) => {row.restarts ?? '-'} }, { key: 'metric', header: '사용량', className: 'w-40 px-5 py-3', cell: (row) => {row.metric ? `${row.metric.cpu} / ${row.metric.memory}` : '-'} }, { key: 'age', header: 'Age', className: 'w-28 px-5 py-3', cell: (row) => {row.age || '-'} }, { key: 'detail', header: '상세', className: 'min-w-[260px] px-5 py-3', cell: (row) => {row.detail || '-'} } ], [] ); const filters = useMemo(() => [NamespaceFilter(rows), KindFilter(rows)], [rows]); return ( `${row.namespace} ${row.kind} ${row.name} ${row.status} ${row.detail}`} getRowKey={(row) => row.id} pageSize={10} minWidth={1280} loading={loading} emptyMessage={emptyMessage} /> ); } export function InfrastructureClusterPage() { const { data, loading, loadingVisible, failed, refresh } = useInfrastructureData(); const totals = useMemo(() => data.runtimes.reduce( (acc, runtime) => ({ namespaces: acc.namespaces + 1, resources: acc.resources + runtime.resources.length, pods: acc.pods + runtime.summary.pods, deployments: acc.deployments + runtime.summary.deployments, services: acc.services + runtime.summary.services, pvc: acc.pvc + runtime.summary.persistentVolumeClaims, failed: acc.failed + (runtime.failed || !runtime.exists ? 1 : 0) }), { namespaces: 0, resources: 0, pods: 0, deployments: 0, services: 0, pvc: 0, failed: 0 } ), [data.runtimes]); return (
{[ ['네임스페이스', totals.namespaces], ['전체 리소스', totals.resources], ['Pod', totals.pods], ['Deployment', totals.deployments], ['Service', totals.services], ['PVC', totals.pvc] ].map(([label, value]) => (

{label}

{value}

))}
{data.runtimes.map((runtime) => (

{runtime.namespace}

{runtime.failed ? '조회 실패' : runtime.exists ? '조회 가능' : '네임스페이스 없음'}

))}
); } function SummaryValue({ label, value }: { label: string; value: number }) { return (

{label}

{value}

); } export function InfrastructureWorkloadsPage() { const { data, loading, loadingVisible, failed, refresh } = useInfrastructureData(); const rows = useMemo(() => rowsFromData(data, isWorkload), [data]); return ( ); } export function InfrastructureNetworkPage() { const { data, loading, loadingVisible, failed, refresh } = useInfrastructureData(); const rows = useMemo(() => rowsFromData(data, isNetwork), [data]); return ( ); } export function InfrastructureStoragePage() { const { data, loading, loadingVisible, failed, refresh } = useInfrastructureData(); const rows = useMemo(() => rowsFromData(data, isStorage), [data]); return ( ); }