This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
createRuntimeProfile,
|
||||
createRuntimeVariable,
|
||||
deleteRuntimeIngressRule,
|
||||
deleteRuntimeProfile,
|
||||
deleteRuntimeVariable,
|
||||
loadApps,
|
||||
loadRuntimeProfiles,
|
||||
@@ -80,6 +81,7 @@ export function RuntimeConfigPage() {
|
||||
const [actionKey, setActionKey] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||
const [deleteProfileTarget, setDeleteProfileTarget] = useState<RuntimeEnvProfile | null>(null);
|
||||
const [deleteVariableTarget, setDeleteVariableTarget] = useState<RuntimeEnvVariable | null>(null);
|
||||
const [deleteIngressTarget, setDeleteIngressTarget] = useState<RuntimeIngressRule | null>(null);
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
@@ -116,6 +118,7 @@ export function RuntimeConfigPage() {
|
||||
profile.environment === profileEnvironment &&
|
||||
profile.namespace.trim() === profileNamespace.trim()
|
||||
));
|
||||
const showDuplicateProfileWarning = Boolean(profileName.trim() && duplicateProfile);
|
||||
|
||||
const refreshResources = (targetNamespace = namespace) => {
|
||||
if (!targetNamespace.trim()) {
|
||||
@@ -235,6 +238,23 @@ export function RuntimeConfigPage() {
|
||||
.catch((createError: Error) => setError(createError.message));
|
||||
};
|
||||
|
||||
const removeProfile = () => {
|
||||
if (!deleteProfileTarget) return;
|
||||
const target = deleteProfileTarget;
|
||||
setActionKey(`profile-delete-${target.id}`);
|
||||
deleteRuntimeProfile(target.id)
|
||||
.then(() => {
|
||||
toast({ type: 'success', title: '프로필 삭제', message: `${target.name} 프로필을 삭제했습니다.` });
|
||||
setDeleteProfileTarget(null);
|
||||
return refreshProfiles(appId);
|
||||
})
|
||||
.catch((deleteError: Error) => {
|
||||
setError(deleteError.message);
|
||||
toast({ type: 'error', title: '프로필 삭제 실패', message: deleteError.message });
|
||||
})
|
||||
.finally(() => setActionKey(''));
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
if (!selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()) return;
|
||||
const profile = selectedProfile;
|
||||
@@ -367,6 +387,13 @@ export function RuntimeConfigPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const openProfileModal = () => {
|
||||
setProfileName('');
|
||||
setProfileEnvironment('dev');
|
||||
setProfileNamespace(selectedApp?.namespace ?? '');
|
||||
setProfileModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -374,7 +401,7 @@ export function RuntimeConfigPage() {
|
||||
title="실행 설정"
|
||||
description="앱 구성요소별 환경변수와 시크릿을 프로필로 묶어 배포 환경에 적용합니다."
|
||||
actions={(
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={() => setProfileModalOpen(true)} disabled={!appId}>
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={openProfileModal} disabled={!appId}>
|
||||
<Plus className="size-4" />
|
||||
프로필 생성
|
||||
</Button>
|
||||
@@ -475,6 +502,19 @@ export function RuntimeConfigPage() {
|
||||
<span>수정 <b className="text-foreground">{formatDate(profile.updatedAt ?? profile.createdAt)}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="size-8 shrink-0 self-end px-0 text-rose-300 hover:text-rose-200 lg:self-center"
|
||||
title="프로필 삭제"
|
||||
aria-label={`${profile.name} 프로필 삭제`}
|
||||
disabled={Boolean(actionKey)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setDeleteProfileTarget(profile);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -699,7 +739,7 @@ export function RuntimeConfigPage() {
|
||||
footer={(
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setProfileModalOpen(false)}>취소</Button>
|
||||
<Button disabled={!profileName.trim() || !profileNamespace.trim() || Boolean(duplicateProfile)} onClick={addProfile}>생성</Button>
|
||||
<Button disabled={!profileName.trim() || !profileNamespace.trim() || showDuplicateProfileWarning} onClick={addProfile}>생성</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
@@ -728,13 +768,36 @@ export function RuntimeConfigPage() {
|
||||
{namespaceOptions.map((item) => <option key={item} value={item} />)}
|
||||
</datalist>
|
||||
</label>
|
||||
{duplicateProfile ? (
|
||||
{showDuplicateProfileWarning ? (
|
||||
<p className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
|
||||
이미 같은 환경과 네임스페이스의 프로필이 있습니다. 기존 프로필을 선택해 설정값을 수정하세요.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={Boolean(deleteProfileTarget)}
|
||||
title="프로필 삭제"
|
||||
onClose={() => {
|
||||
if (!actionKey) setDeleteProfileTarget(null);
|
||||
}}
|
||||
footer={(
|
||||
<>
|
||||
<Button variant="outline" disabled={Boolean(actionKey)} onClick={() => setDeleteProfileTarget(null)}>취소</Button>
|
||||
<Button className="bg-rose-600 text-white hover:bg-rose-500" disabled={Boolean(actionKey)} onClick={removeProfile}>
|
||||
{actionKey.startsWith('profile-delete-') ? '삭제 중' : '삭제'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
<span className="font-semibold text-foreground">{deleteProfileTarget?.name ?? '-'}</span>
|
||||
{' '}프로필을 삭제합니다.
|
||||
</p>
|
||||
<p className="text-muted-foreground">프로필에 등록된 설정값과 접속 설정도 함께 삭제됩니다.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={Boolean(deleteVariableTarget)}
|
||||
title="설정값 삭제"
|
||||
|
||||
@@ -18,6 +18,12 @@ export function createRuntimeProfile(input: { appId: string; name: string; envir
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteRuntimeProfile(profileId: string) {
|
||||
return apiRequest<{ deleted: boolean; profileId: string; name: string }>(`/apps/runtime-profiles/${profileId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
export function createRuntimeVariable(
|
||||
profileId: string,
|
||||
input: { componentId: string; key: string; value: string; isSecret: boolean }
|
||||
|
||||
Reference in New Issue
Block a user