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 { useEffect, useMemo, useState } from 'react';
|
||||||
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
||||||
import { useToast } from '../../components/toast';
|
import { useToast } from '../../components/toast';
|
||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
listLauncherVersions,
|
listLauncherVersions,
|
||||||
loadLauncherComponentLogs,
|
loadLauncherComponentLogs,
|
||||||
loadLauncherRuntimeResources,
|
loadLauncherRuntimeResources,
|
||||||
|
loadRuntimeWorkloadEnv,
|
||||||
restartLauncherComponent,
|
restartLauncherComponent,
|
||||||
|
saveRuntimeWorkloadEnv,
|
||||||
type LauncherBuildJob,
|
type LauncherBuildJob,
|
||||||
type LauncherVersion
|
type LauncherVersion
|
||||||
} from './launcher-api';
|
} from './launcher-api';
|
||||||
@@ -119,6 +121,10 @@ export function LauncherComponentsPage() {
|
|||||||
const [logTarget, setLogTarget] = useState<LauncherComponentDefinition | null>(null);
|
const [logTarget, setLogTarget] = useState<LauncherComponentDefinition | null>(null);
|
||||||
const [logs, setLogs] = useState('');
|
const [logs, setLogs] = useState('');
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
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 loadingVisible = useMinimumLoading(loading || logLoading);
|
||||||
|
|
||||||
const activeVersion = useMemo(() => versions.find((version) => version.isActive) ?? versions[0], [versions]);
|
const activeVersion = useMemo(() => versions.find((version) => version.isActive) ?? versions[0], [versions]);
|
||||||
@@ -171,6 +177,54 @@ export function LauncherComponentsPage() {
|
|||||||
.finally(() => setActionKey(''));
|
.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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
@@ -272,6 +326,12 @@ export function LauncherComponentsPage() {
|
|||||||
<RotateCcw className={cn('size-4', actionKey === component.key && 'animate-spin')} />
|
<RotateCcw className={cn('size-4', actionKey === component.key && 'animate-spin')} />
|
||||||
재시작
|
재시작
|
||||||
</Button>
|
</Button>
|
||||||
|
{component.type === 'backend' ? (
|
||||||
|
<Button variant="outline" onClick={() => openEnv(component)} disabled={!workload}>
|
||||||
|
<Settings className="size-4" />
|
||||||
|
환경변수
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -334,6 +394,94 @@ export function LauncherComponentsPage() {
|
|||||||
</pre>
|
</pre>
|
||||||
</Modal>
|
</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} />
|
<LoadingModal open={loadingVisible} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -314,10 +314,45 @@ export function deleteLauncherCommonSetting(id: string) {
|
|||||||
return apiRequest<{ deleted: boolean }>(`/launcher/common-settings/${id}`, { method: 'DELETE' });
|
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') {
|
export function loadLauncherRuntimeResources(namespace = 'studio-system') {
|
||||||
return apiRequest<RuntimeResourceResponse>(`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources`);
|
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) {
|
export function loadLauncherComponentLogs(namespace: string, kind: string, name: string, tail = 120) {
|
||||||
return apiRequest<RuntimeWorkloadLogResponse>(
|
return apiRequest<RuntimeWorkloadLogResponse>(
|
||||||
`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/logs?tail=${tail}`
|
`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/logs?tail=${tail}`
|
||||||
|
|||||||
Reference in New Issue
Block a user