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

This commit is contained in:
2026-07-09 14:53:43 +09:00
2 changed files with 72 additions and 3 deletions
+66 -3
View File
@@ -14,6 +14,7 @@ import {
createRuntimeProfile, createRuntimeProfile,
createRuntimeVariable, createRuntimeVariable,
deleteRuntimeIngressRule, deleteRuntimeIngressRule,
deleteRuntimeProfile,
deleteRuntimeVariable, deleteRuntimeVariable,
loadApps, loadApps,
loadRuntimeProfiles, loadRuntimeProfiles,
@@ -80,6 +81,7 @@ export function RuntimeConfigPage() {
const [actionKey, setActionKey] = useState(''); const [actionKey, setActionKey] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [profileModalOpen, setProfileModalOpen] = useState(false); const [profileModalOpen, setProfileModalOpen] = useState(false);
const [deleteProfileTarget, setDeleteProfileTarget] = useState<RuntimeEnvProfile | null>(null);
const [deleteVariableTarget, setDeleteVariableTarget] = useState<RuntimeEnvVariable | null>(null); const [deleteVariableTarget, setDeleteVariableTarget] = useState<RuntimeEnvVariable | null>(null);
const [deleteIngressTarget, setDeleteIngressTarget] = useState<RuntimeIngressRule | null>(null); const [deleteIngressTarget, setDeleteIngressTarget] = useState<RuntimeIngressRule | null>(null);
const loadingVisible = useMinimumLoading(loading); const loadingVisible = useMinimumLoading(loading);
@@ -116,6 +118,7 @@ export function RuntimeConfigPage() {
profile.environment === profileEnvironment && profile.environment === profileEnvironment &&
profile.namespace.trim() === profileNamespace.trim() profile.namespace.trim() === profileNamespace.trim()
)); ));
const showDuplicateProfileWarning = Boolean(profileName.trim() && duplicateProfile);
const refreshResources = (targetNamespace = namespace) => { const refreshResources = (targetNamespace = namespace) => {
if (!targetNamespace.trim()) { if (!targetNamespace.trim()) {
@@ -235,6 +238,23 @@ export function RuntimeConfigPage() {
.catch((createError: Error) => setError(createError.message)); .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 = () => { const addVariable = () => {
if (!selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()) return; if (!selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()) return;
const profile = selectedProfile; const profile = selectedProfile;
@@ -367,6 +387,13 @@ export function RuntimeConfigPage() {
}); });
}; };
const openProfileModal = () => {
setProfileName('');
setProfileEnvironment('dev');
setProfileNamespace(selectedApp?.namespace ?? '');
setProfileModalOpen(true);
};
return ( return (
<> <>
<PageTitle <PageTitle
@@ -374,7 +401,7 @@ export function RuntimeConfigPage() {
title="실행 설정" title="실행 설정"
description="앱 구성요소별 환경변수와 시크릿을 프로필로 묶어 배포 환경에 적용합니다." description="앱 구성요소별 환경변수와 시크릿을 프로필로 묶어 배포 환경에 적용합니다."
actions={( 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" /> <Plus className="size-4" />
프로필 생성 프로필 생성
</Button> </Button>
@@ -475,6 +502,19 @@ export function RuntimeConfigPage() {
<span>수정 <b className="text-foreground">{formatDate(profile.updatedAt ?? profile.createdAt)}</b></span> <span>수정 <b className="text-foreground">{formatDate(profile.updatedAt ?? profile.createdAt)}</b></span>
</div> </div>
</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>
</div> </div>
))} ))}
@@ -699,7 +739,7 @@ export function RuntimeConfigPage() {
footer={( footer={(
<> <>
<Button variant="outline" onClick={() => setProfileModalOpen(false)}>취소</Button> <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} />)} {namespaceOptions.map((item) => <option key={item} value={item} />)}
</datalist> </datalist>
</label> </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 className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
이미 같은 환경과 네임스페이스의 프로필이 있습니다. 기존 프로필을 선택해 설정값을 수정하세요. 이미 같은 환경과 네임스페이스의 프로필이 있습니다. 기존 프로필을 선택해 설정값을 수정하세요.
</p> </p>
) : null} ) : null}
</div> </div>
</Modal> </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 <Modal
open={Boolean(deleteVariableTarget)} open={Boolean(deleteVariableTarget)}
title="설정값 삭제" 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( export function createRuntimeVariable(
profileId: string, profileId: string,
input: { componentId: string; key: string; value: string; isSecret: boolean } input: { componentId: string; key: string; value: string; isSecret: boolean }