improve app registration and component service ports
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -15,6 +15,7 @@ type ComponentPortForm = {
|
||||
name: string;
|
||||
port: string;
|
||||
targetPort: string;
|
||||
nodePort: string;
|
||||
protocol: string;
|
||||
};
|
||||
|
||||
@@ -29,6 +30,7 @@ const emptyComponentPort = (): ComponentPortForm => ({
|
||||
name: '',
|
||||
port: '',
|
||||
targetPort: '',
|
||||
nodePort: '',
|
||||
protocol: 'TCP'
|
||||
});
|
||||
|
||||
@@ -252,6 +254,7 @@ function componentPortForms(component: ApiAppComponent): ComponentPortForm[] {
|
||||
name: port.name ?? '',
|
||||
port: port.port ? String(port.port) : '',
|
||||
targetPort: port.targetPort ? String(port.targetPort) : '',
|
||||
nodePort: port.nodePort ? String(port.nodePort) : '',
|
||||
protocol: port.protocol ?? 'TCP'
|
||||
}));
|
||||
}
|
||||
@@ -261,6 +264,7 @@ function componentPortForms(component: ApiAppComponent): ComponentPortForm[] {
|
||||
name: 'http',
|
||||
port: String(component.port),
|
||||
targetPort: String(component.targetPort ?? component.port),
|
||||
nodePort: '',
|
||||
protocol: 'TCP'
|
||||
}];
|
||||
}
|
||||
@@ -271,7 +275,20 @@ function componentPortForms(component: ApiAppComponent): ComponentPortForm[] {
|
||||
function formatComponentPorts(component: ApiAppComponent) {
|
||||
const ports = componentPortForms(component).filter((port) => port.port);
|
||||
if (ports.length === 0) return '-';
|
||||
return ports.map((port) => `${port.name || 'port'} ${port.port}:${port.targetPort || port.port}`).join(', ');
|
||||
return ports.map((port) => {
|
||||
const nodePort = port.nodePort ? ` / node ${port.nodePort}` : '';
|
||||
return `${port.name || 'port'} ${port.port}:${port.targetPort || port.port}${nodePort}`;
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
function appKeyFromName(name: string) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
export function AppManagementPage() {
|
||||
@@ -508,6 +525,13 @@ export function AppManagementPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const changeNewAppName = (name: string) => {
|
||||
const appKey = appKeyFromName(name);
|
||||
setNewAppName(name);
|
||||
setNewAppKey(appKey);
|
||||
setNewAppNamespace(appKey ? `stdo-${appKey}` : '');
|
||||
};
|
||||
|
||||
const createApp = () => {
|
||||
if (!newAppKey.trim() || !newAppName.trim()) return;
|
||||
|
||||
@@ -603,6 +627,7 @@ export function AppManagementPage() {
|
||||
name: item.name.trim() || undefined,
|
||||
port: item.port ? Number(item.port) : undefined,
|
||||
targetPort: item.targetPort ? Number(item.targetPort) : item.port ? Number(item.port) : undefined,
|
||||
nodePort: componentServiceType === 'NodePort' && item.nodePort ? Number(item.nodePort) : undefined,
|
||||
protocol: item.protocol || 'TCP'
|
||||
}))
|
||||
.filter((item) => item.port);
|
||||
@@ -981,7 +1006,7 @@ export function AppManagementPage() {
|
||||
title="앱 구성요소"
|
||||
description="앱을 구성하는 실행 요소와 관련 리소스를 관리합니다."
|
||||
action={
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" disabled={!selectedApp} onClick={() => setIsComponentOpen(true)}>
|
||||
<Button className="h-8 px-3 text-[13px]" disabled={!selectedApp} onClick={() => setIsComponentOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
추가
|
||||
</Button>
|
||||
@@ -1170,20 +1195,20 @@ export function AppManagementPage() {
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/55 px-4 backdrop-blur-[2px]">
|
||||
<div className="w-full max-w-lg rounded-lg border border-border bg-card shadow-xl">
|
||||
<div className="border-b border-border px-5 py-3">
|
||||
<h2 className="text-sm font-semibold">앱 그룹 등록</h2>
|
||||
<h2 className="text-sm font-semibold">앱 등록</h2>
|
||||
</div>
|
||||
<div className="space-y-4 px-5 py-5">
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">앱 Key</span>
|
||||
<input value={newAppKey} onChange={(event) => setNewAppKey(event.target.value)} className="mt-2 h-9 w-full rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary" />
|
||||
<span className="text-xs font-medium text-muted-foreground">앱 이름 <b className="text-foreground">*</b></span>
|
||||
<input value={newAppName} onChange={(event) => changeNewAppName(event.target.value)} className="mt-2 h-9 w-full rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">앱 이름</span>
|
||||
<input value={newAppName} onChange={(event) => setNewAppName(event.target.value)} className="mt-2 h-9 w-full rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary" />
|
||||
<span className="text-xs font-medium text-muted-foreground">앱 Key</span>
|
||||
<input value={newAppKey} readOnly className="mt-2 h-9 w-full rounded-md border border-border bg-muted/40 px-3 font-mono text-sm text-muted-foreground outline-none" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">네임스페이스</span>
|
||||
<input value={newAppNamespace} onChange={(event) => setNewAppNamespace(event.target.value)} className="mt-2 h-9 w-full rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary" />
|
||||
<input value={newAppNamespace} readOnly className="mt-2 h-9 w-full rounded-md border border-border bg-muted/40 px-3 font-mono text-sm text-muted-foreground outline-none" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">설명</span>
|
||||
@@ -1215,11 +1240,11 @@ export function AppManagementPage() {
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">구성요소명</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">구성요소명 <b className="text-foreground">*</b></span>
|
||||
<input value={componentName} onChange={(event) => setComponentName(event.target.value)} className="mt-2 h-9 w-full rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">유형</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">유형 <b className="text-foreground">*</b></span>
|
||||
<select
|
||||
value={componentType}
|
||||
onChange={(event) => {
|
||||
@@ -1246,7 +1271,7 @@ export function AppManagementPage() {
|
||||
</label>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">이미지 소스</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">이미지 소스 <b className="text-foreground">*</b></span>
|
||||
<select
|
||||
value={componentImageSource}
|
||||
onChange={(event) => {
|
||||
@@ -1270,7 +1295,7 @@ export function AppManagementPage() {
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">워크로드</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">워크로드 <b className="text-foreground">*</b></span>
|
||||
<select
|
||||
value={componentWorkloadType}
|
||||
onChange={(event) => {
|
||||
@@ -1311,7 +1336,7 @@ export function AppManagementPage() {
|
||||
</>
|
||||
) : (
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">이미지명</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">이미지명 <b className="text-foreground">*</b></span>
|
||||
<input
|
||||
value={componentExternalImage}
|
||||
onChange={(event) => setComponentExternalImage(event.target.value)}
|
||||
@@ -1343,7 +1368,15 @@ export function AppManagementPage() {
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{componentPorts.map((item, index) => (
|
||||
<div key={index} className="grid min-w-0 gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,0.85fr)_minmax(0,0.85fr)_minmax(0,0.7fr)_2.5rem]">
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'grid min-w-0 gap-2',
|
||||
componentServiceType === 'NodePort'
|
||||
? 'sm:grid-cols-[minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,0.8fr)_minmax(0,0.8fr)_minmax(0,0.65fr)_2.5rem]'
|
||||
: 'sm:grid-cols-[minmax(0,1fr)_minmax(0,0.85fr)_minmax(0,0.85fr)_minmax(0,0.7fr)_2.5rem]'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
value={item.name}
|
||||
onChange={(event) => setComponentPorts((prev) => prev.map((port, portIndex) => portIndex === index ? { ...port, name: event.target.value } : port))}
|
||||
@@ -1366,6 +1399,17 @@ export function AppManagementPage() {
|
||||
className="h-9 min-w-0 rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary"
|
||||
placeholder="컨테이너 포트"
|
||||
/>
|
||||
{componentServiceType === 'NodePort' ? (
|
||||
<input
|
||||
type="number"
|
||||
min="30000"
|
||||
max="32767"
|
||||
value={item.nodePort}
|
||||
onChange={(event) => setComponentPorts((prev) => prev.map((port, portIndex) => portIndex === index ? { ...port, nodePort: event.target.value } : port))}
|
||||
className="h-9 min-w-0 rounded-md border border-border bg-background px-3 text-sm outline-none focus:border-primary"
|
||||
placeholder="노드 포트"
|
||||
/>
|
||||
) : null}
|
||||
<select
|
||||
value={item.protocol}
|
||||
onChange={(event) => setComponentPorts((prev) => prev.map((port, portIndex) => portIndex === index ? { ...port, protocol: event.target.value } : port))}
|
||||
|
||||
@@ -456,19 +456,27 @@ export function ReleaseManagementPage() {
|
||||
|
||||
<Modal open={releaseModalOpen} title="릴리즈 생성" onClose={() => setReleaseModalOpen(false)} footer={<><Button variant="outline" onClick={() => setReleaseModalOpen(false)}>취소</Button><Button disabled={!version || selectedBuildIds.length === 0} onClick={submit}>생성</Button></>}>
|
||||
<div className="space-y-3">
|
||||
<select className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={selectedAppId} onChange={(event) => {
|
||||
selectApp(event.target.value);
|
||||
const appBuilds = candidateBuilds.filter((build) => build.component.app.id === event.target.value);
|
||||
setSelectedBuildIds(latestBuildIdsByComponent(appBuilds));
|
||||
setVersion(newestBuildVersion(appBuilds));
|
||||
}}>
|
||||
{candidateGroups.map((group) => <option key={group.appId} value={group.appId}>{group.appName}</option>)}
|
||||
</select>
|
||||
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">앱 <b className="text-foreground">*</b></span>
|
||||
<select className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={selectedAppId} onChange={(event) => {
|
||||
selectApp(event.target.value);
|
||||
const appBuilds = candidateBuilds.filter((build) => build.component.app.id === event.target.value);
|
||||
setSelectedBuildIds(latestBuildIdsByComponent(appBuilds));
|
||||
setVersion(newestBuildVersion(appBuilds));
|
||||
}}>
|
||||
{candidateGroups.map((group) => <option key={group.appId} value={group.appId}>{group.appName}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">버전 <b className="text-foreground">*</b></span>
|
||||
<input className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
||||
</label>
|
||||
<div className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs leading-5 text-muted-foreground">
|
||||
선택한 develop 커밋을 release로 승격한 뒤 release 브랜치에서 다시 빌드합니다.
|
||||
</div>
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">빌드 선택 <b className="text-foreground">*</b></p>
|
||||
<div className="mt-2 max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
||||
{modalBuilds.length === 0 ? (
|
||||
<div className="px-2 py-8 text-center text-sm text-muted-foreground">
|
||||
등록 가능한 새 빌드 결과가 없습니다.
|
||||
@@ -492,6 +500,7 @@ export function ReleaseManagementPage() {
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -381,7 +381,7 @@ export function RepositoryManagementPage() {
|
||||
</div>
|
||||
<div className="space-y-4 px-5 py-5">
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">저장소명</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">저장소명 <b className="text-foreground">*</b></span>
|
||||
<div className="mt-2 flex h-9 overflow-hidden rounded-md border border-border bg-background focus-within:border-primary">
|
||||
<span className="flex h-full shrink-0 items-center border-r border-border bg-muted px-3 text-xs font-semibold text-muted-foreground">
|
||||
{giteaAppOrg}/
|
||||
|
||||
@@ -76,7 +76,7 @@ export function createComponent(
|
||||
buildTarget?: string;
|
||||
port?: number;
|
||||
targetPort?: number;
|
||||
ports?: Array<{ name?: string; port?: number; targetPort?: number; protocol?: string }>;
|
||||
ports?: Array<{ name?: string; port?: number; targetPort?: number; nodePort?: number; protocol?: string }>;
|
||||
}
|
||||
) {
|
||||
return apiRequest<{ component: ApiAppComponent }>(`/apps/${appId}/components`, {
|
||||
|
||||
Reference in New Issue
Block a user