merge develop into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-14 15:25:12 +09:00
3 changed files with 137 additions and 4 deletions
+22 -1
View File
@@ -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} 설정값` : '설정값'}
+31 -1
View File
@@ -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} 설정값` : '설정값'}