diff --git a/src/App.tsx b/src/App.tsx index b0a2ad3..367aaf8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1932,7 +1932,28 @@ function ServerResourceDetail({ label, value }: { label: string; value: string } function formatResourceDetail(metric?: ClusterMetricValue | null) { if (!metric) return '-'; if (!metric.usage && !metric.capacity) return '-'; - return `${metric.usage ?? '-'} / ${metric.capacity ?? '-'}`; + return `${formatBinaryQuantity(metric.usage)} / ${formatBinaryQuantity(metric.capacity)}`; +} + +function formatBinaryQuantity(value?: string | null) { + if (!value) return '-'; + const matched = value.match(/^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)$/); + if (!matched) return value; + + const numeric = Number(matched[1]); + const mi = matched[2] === 'Ki' + ? numeric / 1024 + : matched[2] === 'Mi' + ? numeric + : matched[2] === 'Gi' + ? numeric * 1024 + : numeric * 1024 * 1024; + const format = (amount: number) => Number(amount.toFixed(1)).toLocaleString('ko-KR'); + + if (mi >= 1024 * 1024) return `${format(mi / 1024 / 1024)} TiB`; + if (mi >= 1024) return `${format(mi / 1024)} GiB`; + if (mi >= 1) return `${format(mi)} MiB`; + return `${format(mi * 1024)} KiB`; } function ContainerPanel({ resources, loading, onOpen }: { resources: DashboardRuntimeResource[]; loading: boolean; onOpen: () => void }) { diff --git a/src/pages/common-apps/CommonAppComponentsPage.tsx b/src/pages/common-apps/CommonAppComponentsPage.tsx index cfdfe15..bd76f1e 100644 --- a/src/pages/common-apps/CommonAppComponentsPage.tsx +++ b/src/pages/common-apps/CommonAppComponentsPage.tsx @@ -1,4 +1,4 @@ -import { Plus, RotateCcw, Server, Settings, Trash2 } from 'lucide-react'; +import { Plus, RotateCcw, Server, Settings, TerminalSquare, Trash2 } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui'; import { useToast } from '../../components/toast'; @@ -8,7 +8,9 @@ import type { RuntimeResource } from '../../types/runtime'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; import { loadLauncherRuntimeResources, + loadLauncherComponentLogs, loadRuntimeWorkloadEnv, + restartLauncherComponent, saveRuntimeWorkloadEnv } from '../launcher/launcher-api'; import { COMMON_APP_NAMESPACE, commonAppComponents, type CommonAppComponent } from './common-app-utils'; @@ -24,6 +26,10 @@ function apiKind(kind: string) { return kind.toLowerCase(); } +function stripAnsi(value: string) { + return value.replace(/(?:\x1B|\u241B)\[[0-?]*[ -/]*[@-~]/g, ''); +} + function findResource(resources: RuntimeResource[], kind: string, name: string) { return resources.find((resource) => resource.kind === kind && resource.name === name); } @@ -53,7 +59,12 @@ export function CommonAppComponentsPage() { const { toast } = useToast(); const [resources, setResources] = useState([]); const [loading, setLoading] = useState(true); + const [logTarget, setLogTarget] = useState(null); + const [logs, setLogs] = useState(''); + const [logLoading, setLogLoading] = useState(false); const [envTarget, setEnvTarget] = useState(null); + const [restartTarget, setRestartTarget] = useState(null); + const [restartKey, setRestartKey] = useState(''); const [envVariables, setEnvVariables] = useState([]); const [envLoading, setEnvLoading] = useState(false); const [envSaving, setEnvSaving] = useState(false); @@ -89,6 +100,16 @@ export function CommonAppComponentsPage() { .finally(() => setEnvLoading(false)); }; + const openLogs = (component: CommonAppComponent) => { + setLogTarget(component); + setLogs(''); + setLogLoading(true); + loadLauncherComponentLogs(COMMON_APP_NAMESPACE, apiKind(component.kind), component.workloadName) + .then((result) => setLogs(stripAnsi(result.logs || '로그가 없습니다.'))) + .catch((error: Error) => setLogs(`[warn] 로그 조회 실패\n${error.message}`)) + .finally(() => setLogLoading(false)); + }; + const updateEnvVariable = (index: number, patch: Partial>) => { setEnvVariables((prev) => prev.map((variable, variableIndex) => ( variableIndex === index ? { ...variable, ...patch } : variable @@ -107,6 +128,19 @@ export function CommonAppComponentsPage() { }); }; + const restart = (component: CommonAppComponent) => { + setRestartKey(component.key); + restartLauncherComponent(COMMON_APP_NAMESPACE, apiKind(component.kind), component.workloadName) + .then(() => { + toast({ type: 'success', message: `${component.name} 재시작을 요청했습니다.` }); + refresh(); + }) + .catch((error: Error) => { + toast({ type: 'error', title: '재시작 실패', message: error.message }); + }) + .finally(() => setRestartKey('')); + }; + const saveEnv = () => { if (!envTarget) return; setEnvSaving(true); @@ -185,7 +219,15 @@ export function CommonAppComponentsPage() { -
+
+ +
+ setLogTarget(null)} + className="!max-w-5xl" + > +
+          {logs || (logLoading ? '로그를 불러오는 중...' : '로그가 없습니다.')}
+        
+
+ + { + if (!restartTarget || restartKey !== restartTarget.key) setRestartTarget(null); + }} + footer={ + <> + + + + } + > +

+ {restartTarget?.name}을 재시작합니다. 실행 중인 요청은 잠시 영향을 받을 수 있습니다. +

+
+ (null); + const [restartTarget, setRestartTarget] = useState(null); const [envVariables, setEnvVariables] = useState>([]); const [envLoading, setEnvLoading] = useState(false); const [envSaving, setEnvSaving] = useState(false); @@ -351,7 +352,7 @@ export function LauncherComponentsPage() { 로그 - @@ -423,6 +424,35 @@ export function LauncherComponentsPage() { + { + if (!restartTarget || actionKey !== restartTarget.key) setRestartTarget(null); + }} + footer={ + <> + + + + } + > +

+ {restartTarget?.name}을 재시작합니다. 실행 중인 요청은 잠시 영향을 받을 수 있습니다. +

+
+