This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Database, FileArchive, RotateCcw, Server, TerminalSquare } from 'lucide-react';
|
||||
import { Database, FileArchive, Plus, RotateCcw, Server, Settings, TerminalSquare, Trash2 } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
listLauncherVersions,
|
||||
loadLauncherComponentLogs,
|
||||
loadLauncherRuntimeResources,
|
||||
loadRuntimeWorkloadEnv,
|
||||
restartLauncherComponent,
|
||||
saveRuntimeWorkloadEnv,
|
||||
type LauncherBuildJob,
|
||||
type LauncherVersion
|
||||
} from './launcher-api';
|
||||
@@ -119,6 +121,10 @@ export function LauncherComponentsPage() {
|
||||
const [logTarget, setLogTarget] = useState<LauncherComponentDefinition | null>(null);
|
||||
const [logs, setLogs] = useState('');
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
const [envTarget, setEnvTarget] = useState<LauncherComponentDefinition | null>(null);
|
||||
const [envVariables, setEnvVariables] = useState<Array<{ key: string; value: string; editable: boolean; source: string }>>([]);
|
||||
const [envLoading, setEnvLoading] = useState(false);
|
||||
const [envSaving, setEnvSaving] = useState(false);
|
||||
const loadingVisible = useMinimumLoading(loading || logLoading);
|
||||
|
||||
const activeVersion = useMemo(() => versions.find((version) => version.isActive) ?? versions[0], [versions]);
|
||||
@@ -171,6 +177,54 @@ export function LauncherComponentsPage() {
|
||||
.finally(() => setActionKey(''));
|
||||
};
|
||||
|
||||
const openEnv = (component: LauncherComponentDefinition) => {
|
||||
if (!component.kind || !component.workloadName) return;
|
||||
setEnvTarget(component);
|
||||
setEnvVariables([]);
|
||||
setEnvLoading(true);
|
||||
loadRuntimeWorkloadEnv(LAUNCHER_NAMESPACE, apiKind(component.kind), component.workloadName)
|
||||
.then((result) => {
|
||||
setEnvVariables(result.variables ?? []);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast({ type: 'error', title: '환경변수 조회 실패', message: error.message });
|
||||
})
|
||||
.finally(() => setEnvLoading(false));
|
||||
};
|
||||
|
||||
const updateEnvVariable = (index: number, patch: Partial<{ key: string; value: string }>) => {
|
||||
setEnvVariables((prev) => prev.map((variable, variableIndex) => (
|
||||
variableIndex === index ? { ...variable, ...patch } : variable
|
||||
)));
|
||||
};
|
||||
|
||||
const removeEnvVariable = (index: number) => {
|
||||
setEnvVariables((prev) => prev.filter((_, variableIndex) => variableIndex !== index));
|
||||
};
|
||||
|
||||
const saveEnv = () => {
|
||||
if (!envTarget?.kind || !envTarget.workloadName) return;
|
||||
setEnvSaving(true);
|
||||
saveRuntimeWorkloadEnv(
|
||||
LAUNCHER_NAMESPACE,
|
||||
apiKind(envTarget.kind),
|
||||
envTarget.workloadName,
|
||||
envVariables
|
||||
.filter((variable) => variable.editable)
|
||||
.map((variable) => ({ key: variable.key.trim(), value: variable.value }))
|
||||
.filter((variable) => variable.key)
|
||||
)
|
||||
.then((result) => {
|
||||
setEnvVariables(result.variables ?? []);
|
||||
toast({ type: 'success', title: '환경변수 저장', message: `${envTarget.name} 환경변수를 반영했습니다.` });
|
||||
refresh();
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast({ type: 'error', title: '환경변수 저장 실패', message: error.message });
|
||||
})
|
||||
.finally(() => setEnvSaving(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -272,6 +326,12 @@ export function LauncherComponentsPage() {
|
||||
<RotateCcw className={cn('size-4', actionKey === component.key && 'animate-spin')} />
|
||||
재시작
|
||||
</Button>
|
||||
{component.type === 'backend' ? (
|
||||
<Button variant="outline" onClick={() => openEnv(component)} disabled={!workload}>
|
||||
<Settings className="size-4" />
|
||||
환경변수
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -334,6 +394,94 @@ export function LauncherComponentsPage() {
|
||||
</pre>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(envTarget)}
|
||||
title={envTarget ? `${envTarget.name} 환경변수` : '환경변수'}
|
||||
onClose={() => setEnvTarget(null)}
|
||||
className="!max-w-4xl"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setEnvTarget(null)}>닫기</Button>
|
||||
<Button onClick={saveEnv} disabled={envLoading || envSaving}>
|
||||
{envSaving ? '저장 중' : '저장'}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
저장하면 Kubernetes Deployment 환경변수가 변경되고 새 Pod로 롤아웃됩니다. Secret/ConfigMap에서 주입된 값은 여기서 직접 수정하지 않습니다.
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-md border border-border">
|
||||
<table className="w-full min-w-[720px] text-left text-sm">
|
||||
<thead className="border-b border-border bg-muted/30 text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">Key</th>
|
||||
<th className="px-4 py-3 font-medium">Value</th>
|
||||
<th className="w-24 px-4 py-3 font-medium">Source</th>
|
||||
<th className="w-16 px-4 py-3 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{envLoading ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">환경변수를 불러오는 중입니다.</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{!envLoading && envVariables.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">등록된 환경변수가 없습니다.</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{!envLoading && envVariables.map((variable, index) => (
|
||||
<tr key={`${variable.key}-${index}`}>
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
className="h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-xs outline-none focus:ring-2 focus:ring-ring disabled:opacity-60"
|
||||
value={variable.key}
|
||||
disabled={!variable.editable}
|
||||
onChange={(event) => updateEnvVariable(index, { key: event.target.value })}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
className="h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-xs outline-none focus:ring-2 focus:ring-ring disabled:opacity-60"
|
||||
value={variable.value}
|
||||
disabled={!variable.editable}
|
||||
onChange={(event) => updateEnvVariable(index, { value: event.target.value })}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge tone={variable.editable ? 'blue' : 'neutral'}>{variable.source}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8 px-0"
|
||||
disabled={!variable.editable}
|
||||
onClick={() => removeEnvVariable(index)}
|
||||
aria-label="환경변수 삭제"
|
||||
title="환경변수 삭제"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEnvVariables((prev) => [...prev, { key: '', value: '', editable: true, source: 'literal' }])}
|
||||
disabled={envLoading}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
환경변수 추가
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<LoadingModal open={loadingVisible} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -314,10 +314,45 @@ export function deleteLauncherCommonSetting(id: string) {
|
||||
return apiRequest<{ deleted: boolean }>(`/launcher/common-settings/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
|
||||
export type RuntimeWorkloadEnvVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
editable: boolean;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export function loadLauncherRuntimeResources(namespace = 'studio-system') {
|
||||
return apiRequest<RuntimeResourceResponse>(`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources`);
|
||||
}
|
||||
|
||||
|
||||
export function loadRuntimeWorkloadEnv(namespace: string, kind: string, name: string) {
|
||||
return apiRequest<{
|
||||
namespace: string;
|
||||
target: string;
|
||||
container: string | null;
|
||||
variables: RuntimeWorkloadEnvVariable[];
|
||||
}>(`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/env`);
|
||||
}
|
||||
|
||||
export function saveRuntimeWorkloadEnv(
|
||||
namespace: string,
|
||||
kind: string,
|
||||
name: string,
|
||||
variables: Array<{ key: string; value: string }>
|
||||
) {
|
||||
return apiRequest<{
|
||||
namespace: string;
|
||||
target: string;
|
||||
container: string | null;
|
||||
variables: RuntimeWorkloadEnvVariable[];
|
||||
}>(`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/env`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ variables })
|
||||
});
|
||||
}
|
||||
|
||||
export function loadLauncherComponentLogs(namespace: string, kind: string, name: string, tail = 120) {
|
||||
return apiRequest<RuntimeWorkloadLogResponse>(
|
||||
`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/logs?tail=${tail}`
|
||||
|
||||
Reference in New Issue
Block a user