improve launcher history and runtime config UX
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Play, Plus, RotateCcw, Square, Trash2 } from 'lucide-react';
|
||||
import { Plus, RotateCcw, Square, Trash2 } from 'lucide-react';
|
||||
import { Badge, Button, Card, Modal } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
import { PageTitle } from '../../layout/AppLayout';
|
||||
@@ -161,7 +161,7 @@ export function RuntimeConfigPage() {
|
||||
setProfileNamespace(nextApp?.namespace ?? '');
|
||||
setVariableComponentId(nextApp?.components?.[0]?.id ?? '');
|
||||
setIngressComponentId(nextApp?.components?.[0]?.id ?? '');
|
||||
setIngressPort(nextApp?.components?.[0]?.port ? String(nextApp.components[0].port) : '');
|
||||
setIngressPort('');
|
||||
return nextAppId ? loadRuntimeProfiles(nextAppId) : Promise.resolve([]);
|
||||
})
|
||||
.then((nextProfiles) => applyProfiles(nextProfiles))
|
||||
@@ -176,7 +176,7 @@ export function RuntimeConfigPage() {
|
||||
setProfileNamespace(nextApp?.namespace ?? '');
|
||||
setVariableComponentId(nextApp?.components?.[0]?.id ?? '');
|
||||
setIngressComponentId(nextApp?.components?.[0]?.id ?? '');
|
||||
setIngressPort(nextApp?.components?.[0]?.port ? String(nextApp.components[0].port) : '');
|
||||
setIngressPort('');
|
||||
refreshProfiles(nextAppId).catch((loadError: Error) => setError(loadError.message));
|
||||
};
|
||||
|
||||
@@ -209,14 +209,17 @@ export function RuntimeConfigPage() {
|
||||
|
||||
const addVariable = () => {
|
||||
if (!selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()) return;
|
||||
const profile = selectedProfile;
|
||||
const key = variableKey.trim();
|
||||
setError('');
|
||||
setActionKey(`variable-add-${profile.id}`);
|
||||
createRuntimeVariable(selectedProfile.id, {
|
||||
componentId: variableComponentId,
|
||||
key,
|
||||
value: variableValue.trim(),
|
||||
isSecret: variableIsSecret
|
||||
})
|
||||
.then(() => applyRuntimeProfile(profile.id))
|
||||
.then(() => {
|
||||
setVariableKey('');
|
||||
setVariableValue('');
|
||||
@@ -224,58 +227,80 @@ export function RuntimeConfigPage() {
|
||||
toast({
|
||||
type: 'success',
|
||||
title: '설정값 등록',
|
||||
message: `${key} 설정값을 추가했습니다. 실행 중인 파드에는 적용 버튼을 눌러야 반영됩니다.`
|
||||
message: `${key} 설정값을 추가하고 클러스터 설정에 반영했습니다.`
|
||||
});
|
||||
refreshProfiles(appId, selectedProfile.id);
|
||||
refreshResources(profile.namespace);
|
||||
refreshProfiles(appId, profile.id);
|
||||
})
|
||||
.catch((createError: Error) => {
|
||||
setError(createError.message);
|
||||
toast({ type: 'error', title: '설정값 등록 실패', message: createError.message });
|
||||
});
|
||||
})
|
||||
.finally(() => setActionKey(''));
|
||||
};
|
||||
|
||||
const removeVariable = (variableId: string) => {
|
||||
if (!selectedProfile) return;
|
||||
deleteRuntimeVariable(selectedProfile.id, variableId)
|
||||
.then(() => refreshProfiles(appId, selectedProfile.id))
|
||||
.catch((deleteError: Error) => setError(deleteError.message));
|
||||
const profile = selectedProfile;
|
||||
setActionKey(`variable-delete-${variableId}`);
|
||||
deleteRuntimeVariable(profile.id, variableId)
|
||||
.then(() => applyRuntimeProfile(profile.id))
|
||||
.then(() => {
|
||||
toast({ type: 'success', title: '설정값 삭제', message: '설정값을 삭제하고 클러스터 설정에 반영했습니다.' });
|
||||
refreshResources(profile.namespace);
|
||||
refreshProfiles(appId, profile.id);
|
||||
})
|
||||
.catch((deleteError: Error) => {
|
||||
setError(deleteError.message);
|
||||
toast({ type: 'error', title: '설정값 삭제 실패', message: deleteError.message });
|
||||
})
|
||||
.finally(() => setActionKey(''));
|
||||
};
|
||||
|
||||
const addIngressRule = () => {
|
||||
if (!selectedProfile || !ingressComponentId || !ingressHost.trim() || !ingressPort.trim()) return;
|
||||
createRuntimeIngressRule(selectedProfile.id, {
|
||||
const profile = selectedProfile;
|
||||
const host = ingressHost.trim();
|
||||
setActionKey(`ingress-add-${profile.id}`);
|
||||
createRuntimeIngressRule(profile.id, {
|
||||
componentId: ingressComponentId,
|
||||
host: ingressHost.trim(),
|
||||
host,
|
||||
path: ingressPath.trim() || '/',
|
||||
servicePort: Number(ingressPort),
|
||||
stripPrefix: ingressStripPrefix,
|
||||
tlsEnabled: false
|
||||
})
|
||||
.then(() => applyRuntimeProfile(profile.id))
|
||||
.then(() => {
|
||||
setIngressHost('');
|
||||
setIngressPath('/');
|
||||
setIngressStripPrefix(false);
|
||||
refreshProfiles(appId, selectedProfile.id);
|
||||
toast({ type: 'success', title: '접속 설정 등록', message: `${host} 접속 설정을 추가하고 클러스터 설정에 반영했습니다.` });
|
||||
refreshResources(profile.namespace);
|
||||
refreshProfiles(appId, profile.id);
|
||||
})
|
||||
.catch((createError: Error) => setError(createError.message));
|
||||
.catch((createError: Error) => {
|
||||
setError(createError.message);
|
||||
toast({ type: 'error', title: '접속 설정 등록 실패', message: createError.message });
|
||||
})
|
||||
.finally(() => setActionKey(''));
|
||||
};
|
||||
|
||||
const removeIngressRule = (ruleId: string) => {
|
||||
if (!selectedProfile) return;
|
||||
deleteRuntimeIngressRule(selectedProfile.id, ruleId)
|
||||
.then(() => refreshProfiles(appId, selectedProfile.id))
|
||||
.catch((deleteError: Error) => setError(deleteError.message));
|
||||
};
|
||||
|
||||
const applyProfile = (profile = selectedProfile) => {
|
||||
if (!profile) return;
|
||||
setActionKey(`profile-${profile.id}`);
|
||||
applyRuntimeProfile(profile.id)
|
||||
const profile = selectedProfile;
|
||||
setActionKey(`ingress-delete-${ruleId}`);
|
||||
deleteRuntimeIngressRule(profile.id, ruleId)
|
||||
.then(() => applyRuntimeProfile(profile.id))
|
||||
.then(() => {
|
||||
toast({ type: 'success', title: '접속 설정 삭제', message: '접속 설정을 삭제하고 클러스터 설정에 반영했습니다.' });
|
||||
refreshResources(profile.namespace);
|
||||
refreshProfiles(appId, profile.id);
|
||||
})
|
||||
.catch((applyError: Error) => setError(applyError.message))
|
||||
.catch((deleteError: Error) => {
|
||||
setError(deleteError.message);
|
||||
toast({ type: 'error', title: '접속 설정 삭제 실패', message: deleteError.message });
|
||||
})
|
||||
.finally(() => setActionKey(''));
|
||||
};
|
||||
|
||||
@@ -422,20 +447,6 @@ export function RuntimeConfigPage() {
|
||||
<span>수정 <b className="text-foreground">{formatDate(profile.updatedAt ?? profile.createdAt)}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-3 text-[13px]"
|
||||
disabled={actionKey === `profile-${profile.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
applyProfile(profile);
|
||||
}}
|
||||
>
|
||||
<Play className="size-4" />
|
||||
적용
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -478,7 +489,7 @@ export function RuntimeConfigPage() {
|
||||
<td className="px-5 py-3 font-mono text-xs text-muted-foreground">{variable.isSecret ? '********' : variable.value}</td>
|
||||
<td className="px-5 py-3 text-xs text-muted-foreground">{formatDate(variable.createdAt)}</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 text-rose-300 hover:text-rose-200" title="삭제" aria-label={`${variable.key} 삭제`} onClick={() => removeVariable(variable.id)}>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 text-rose-300 hover:text-rose-200" title="삭제" aria-label={`${variable.key} 삭제`} onClick={() => removeVariable(variable.id)} disabled={Boolean(actionKey)}>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</td>
|
||||
@@ -524,9 +535,9 @@ export function RuntimeConfigPage() {
|
||||
placeholder="값 입력"
|
||||
/>
|
||||
</label>
|
||||
<Button className="h-9 w-full text-[13px]" onClick={addVariable} disabled={!selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()}>
|
||||
<Button className="h-9 w-full text-[13px]" onClick={addVariable} disabled={Boolean(actionKey) || !selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()}>
|
||||
<Plus className="size-4" />
|
||||
추가
|
||||
{actionKey.startsWith('variable-add-') ? '저장 중' : '추가'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -570,7 +581,7 @@ export function RuntimeConfigPage() {
|
||||
<td className="px-5 py-3 text-muted-foreground">{rule.servicePort}</td>
|
||||
<td className="px-5 py-3 text-muted-foreground">{rule.stripPrefix ? '제거' : '유지'}</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 text-rose-300 hover:text-rose-200" title="삭제" aria-label={`${rule.host} 삭제`} onClick={() => removeIngressRule(rule.id)}>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 text-rose-300 hover:text-rose-200" title="삭제" aria-label={`${rule.host} 삭제`} onClick={() => removeIngressRule(rule.id)} disabled={Boolean(actionKey)}>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</td>
|
||||
@@ -592,9 +603,8 @@ export function RuntimeConfigPage() {
|
||||
value={ingressComponentId}
|
||||
onChange={(event) => {
|
||||
const nextId = event.target.value;
|
||||
const component = components.find((item) => item.id === nextId);
|
||||
setIngressComponentId(nextId);
|
||||
setIngressPort(component?.port ? String(component.port) : '');
|
||||
setIngressPort('');
|
||||
}}
|
||||
>
|
||||
{components.map((component) => <option key={component.id} value={component.id}>{component.name}</option>)}
|
||||
@@ -624,9 +634,9 @@ export function RuntimeConfigPage() {
|
||||
/>
|
||||
Path prefix 제거
|
||||
</label>
|
||||
<Button className="h-9 w-full text-[13px]" onClick={addIngressRule} disabled={!selectedProfile || !ingressComponentId || !ingressHost.trim() || !ingressPort.trim()}>
|
||||
<Button className="h-9 w-full text-[13px]" onClick={addIngressRule} disabled={Boolean(actionKey) || !selectedProfile || !ingressComponentId || !ingressHost.trim() || !ingressPort.trim()}>
|
||||
<Plus className="size-4" />
|
||||
추가
|
||||
{actionKey.startsWith('ingress-add-') ? '저장 중' : '추가'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user