This commit is contained in:
+22
-1
@@ -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 }) {
|
||||
|
||||
@@ -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<RuntimeResource[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [logTarget, setLogTarget] = useState<CommonAppComponent | null>(null);
|
||||
const [logs, setLogs] = useState('');
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
const [envTarget, setEnvTarget] = useState<CommonAppComponent | null>(null);
|
||||
const [restartTarget, setRestartTarget] = useState<CommonAppComponent | null>(null);
|
||||
const [restartKey, setRestartKey] = useState('');
|
||||
const [envVariables, setEnvVariables] = useState<EnvVariable[]>([]);
|
||||
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<Pick<EnvVariable, 'key' | 'value' | 'isSecret'>>) => {
|
||||
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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button variant="outline" onClick={() => openLogs(component)} disabled={!workload}>
|
||||
<TerminalSquare className="size-4" />
|
||||
로그
|
||||
</Button>
|
||||
<Button onClick={() => setRestartTarget(component)} disabled={!workload || restartKey === component.key}>
|
||||
<RotateCcw className={cn('size-4', restartKey === component.key && 'animate-spin')} />
|
||||
재시작
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => openEnv(component)} disabled={!workload}>
|
||||
<Settings className="size-4" />
|
||||
설정값
|
||||
@@ -230,6 +272,46 @@ export function CommonAppComponentsPage() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={Boolean(logTarget)}
|
||||
title={logTarget ? `${logTarget.name} 로그` : '로그'}
|
||||
onClose={() => setLogTarget(null)}
|
||||
className="!max-w-5xl"
|
||||
>
|
||||
<pre className="max-h-[62vh] overflow-auto rounded-md bg-background p-4 font-mono text-xs leading-6 text-muted-foreground">
|
||||
{logs || (logLoading ? '로그를 불러오는 중...' : '로그가 없습니다.')}
|
||||
</pre>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(restartTarget)}
|
||||
title={restartTarget ? `${restartTarget.name} 재시작` : '구성요소 재시작'}
|
||||
onClose={() => {
|
||||
if (!restartTarget || restartKey !== restartTarget.key) setRestartTarget(null);
|
||||
}}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setRestartTarget(null)} disabled={Boolean(restartTarget && restartKey === restartTarget.key)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!restartTarget) return;
|
||||
restart(restartTarget);
|
||||
setRestartTarget(null);
|
||||
}}
|
||||
disabled={Boolean(restartTarget && restartKey === restartTarget.key)}
|
||||
>
|
||||
재시작
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{restartTarget?.name}을 재시작합니다. 실행 중인 요청은 잠시 영향을 받을 수 있습니다.
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(envTarget)}
|
||||
title={envTarget ? `${envTarget.name} 설정값` : '설정값'}
|
||||
|
||||
@@ -139,6 +139,7 @@ export function LauncherComponentsPage() {
|
||||
const [logs, setLogs] = useState('');
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
const [envTarget, setEnvTarget] = useState<LauncherComponentDefinition | null>(null);
|
||||
const [restartTarget, setRestartTarget] = useState<LauncherComponentDefinition | null>(null);
|
||||
const [envVariables, setEnvVariables] = useState<Array<{ key: string; value: string; isSecret: boolean; editable: boolean; source: string }>>([]);
|
||||
const [envLoading, setEnvLoading] = useState(false);
|
||||
const [envSaving, setEnvSaving] = useState(false);
|
||||
@@ -351,7 +352,7 @@ export function LauncherComponentsPage() {
|
||||
<TerminalSquare className="size-4" />
|
||||
로그
|
||||
</Button>
|
||||
<Button onClick={() => restart(component)} disabled={!workload || actionKey === component.key}>
|
||||
<Button onClick={() => setRestartTarget(component)} disabled={!workload || actionKey === component.key}>
|
||||
<RotateCcw className={cn('size-4', actionKey === component.key && 'animate-spin')} />
|
||||
재시작
|
||||
</Button>
|
||||
@@ -423,6 +424,35 @@ export function LauncherComponentsPage() {
|
||||
</pre>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(restartTarget)}
|
||||
title={restartTarget ? `${restartTarget.name} 재시작` : '구성요소 재시작'}
|
||||
onClose={() => {
|
||||
if (!restartTarget || actionKey !== restartTarget.key) setRestartTarget(null);
|
||||
}}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setRestartTarget(null)} disabled={Boolean(restartTarget && actionKey === restartTarget.key)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!restartTarget) return;
|
||||
restart(restartTarget);
|
||||
setRestartTarget(null);
|
||||
}}
|
||||
disabled={Boolean(restartTarget && actionKey === restartTarget.key)}
|
||||
>
|
||||
재시작
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{restartTarget?.name}을 재시작합니다. 실행 중인 요청은 잠시 영향을 받을 수 있습니다.
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(envTarget)}
|
||||
title={envTarget ? `${envTarget.name} 설정값` : '설정값'}
|
||||
|
||||
Reference in New Issue
Block a user