add runtime profile deletion and refine config forms
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 14:53:39 +09:00
parent f36e15f1ed
commit 009f698a9f
2 changed files with 72 additions and 3 deletions
+66 -3
View File
@@ -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="설정값 삭제"
+6
View File
@@ -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 }