From 2b7f50400710307c6dee465761e0d58a144f3a1d Mon Sep 17 00:00:00 2001 From: chyoo Date: Thu, 2 Jul 2026 15:43:29 +0900 Subject: [PATCH] add build env and path ingress controls --- src/pages/applications/AppManagementPage.tsx | 199 ++++++++++++++++++- src/pages/runtime/RuntimeConfigPage.tsx | 18 +- src/pages/runtime/runtime-api.ts | 2 +- src/types/runtime.ts | 12 ++ 4 files changed, 225 insertions(+), 6 deletions(-) diff --git a/src/pages/applications/AppManagementPage.tsx b/src/pages/applications/AppManagementPage.tsx index f9a36d9..12590fc 100644 --- a/src/pages/applications/AppManagementPage.tsx +++ b/src/pages/applications/AppManagementPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Database, Pencil, Play, Plus, RotateCcw, Square, Trash2 } from 'lucide-react'; +import { Database, Pencil, Play, Plus, RotateCcw, Settings2, Square, Trash2 } from 'lucide-react'; import { Badge, Button, Card, CardHeader } from '../../components/ui'; import { useToast } from '../../components/toast'; import { PageTitle } from '../../layout/AppLayout'; @@ -7,7 +7,7 @@ import { apiBaseUrl, apiFetch, authHeaders } from '../../lib/api'; import { filterAppsByCurrentAdmin, grantAppPermissionToCurrentAdmin } from '../../lib/admin-app-permissions'; import { cn } from '../../lib/utils'; import { getStoredSelectedAppId, setStoredSelectedAppId } from '../../lib/selected-app'; -import type { ApiApp, ApiAppComponent, RuntimePersistentVolumeClaim, RuntimeResource } from '../../types/runtime'; +import type { ApiApp, ApiAppComponent, BuildEnvVariable, RuntimePersistentVolumeClaim, RuntimeResource } from '../../types/runtime'; import { LoadingModal } from '../operations/operation-utils'; type ComponentPortForm = { @@ -17,6 +17,13 @@ type ComponentPortForm = { protocol: string; }; +type BuildEnvVariableForm = { + id?: string; + key: string; + value: string; + isSecret: boolean; +}; + const emptyComponentPort = (): ComponentPortForm => ({ name: '', port: '', @@ -24,6 +31,12 @@ const emptyComponentPort = (): ComponentPortForm => ({ protocol: 'TCP' }); +const emptyBuildEnvVariable = (): BuildEnvVariableForm => ({ + key: '', + value: '', + isSecret: false +}); + function useMinimumLoading(loading: boolean, minimumMs = 500) { const [visible, setVisible] = useState(loading); @@ -148,6 +161,42 @@ function deleteWorkloadPersistentVolumeClaim(namespace: string, kind: string, na }); } +function loadComponentBuildEnv(componentId: string) { + return apiFetch(`${apiBaseUrl}/apps/components/${encodeURIComponent(componentId)}/build-env`, { + headers: authHeaders() + }).then(async (response) => { + if (!response.ok) { + const body = await response.json().catch(() => null); + const message = Array.isArray(body?.message) ? body.message.join(', ') : body?.message; + throw new Error(message || '빌드 환경변수 조회 요청이 실패했습니다.'); + } + return response.json() as Promise<{ variables: BuildEnvVariable[] }>; + }); +} + +function saveComponentBuildEnv(componentId: string, variables: BuildEnvVariableForm[]) { + return apiFetch(`${apiBaseUrl}/apps/components/${encodeURIComponent(componentId)}/build-env`, { + method: 'PUT', + headers: authHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify({ + variables: variables + .map((variable) => ({ + key: variable.key.trim(), + value: variable.value, + isSecret: variable.isSecret + })) + .filter((variable) => variable.key) + }) + }).then(async (response) => { + if (!response.ok) { + const body = await response.json().catch(() => null); + const message = Array.isArray(body?.message) ? body.message.join(', ') : body?.message; + throw new Error(message || '빌드 환경변수 저장 요청이 실패했습니다.'); + } + return response.json() as Promise<{ variables: BuildEnvVariable[] }>; + }); +} + function isRunningResource(resource: RuntimeResource) { if (!['Pod', 'Deployment', 'StatefulSet', 'DaemonSet'].includes(resource.kind)) return false; return ['Running', 'Pending', 'ContainerCreating'].includes(resource.status) || !resource.ready.endsWith('/0'); @@ -251,6 +300,9 @@ export function AppManagementPage() { const [componentPorts, setComponentPorts] = useState([emptyComponentPort()]); const [componentSaveStatus, setComponentSaveStatus] = useState<'idle' | 'saving' | 'error'>('idle'); const [editingComponent, setEditingComponent] = useState(null); + const [buildEnvComponent, setBuildEnvComponent] = useState(null); + const [buildEnvVariables, setBuildEnvVariables] = useState([emptyBuildEnvVariable()]); + const [buildEnvStatus, setBuildEnvStatus] = useState<'idle' | 'loading' | 'saving' | 'error'>('idle'); const [deletingApp, setDeletingApp] = useState(null); const [deletingComponent, setDeletingComponent] = useState(null); const [runtimeResources, setRuntimeResources] = useState([]); @@ -676,6 +728,62 @@ export function AppManagementPage() { }); }; + const openBuildEnvModal = (component: ApiAppComponent) => { + setBuildEnvComponent(component); + setBuildEnvStatus('loading'); + const fallbackVariables = component.buildVariables?.length + ? component.buildVariables.map((variable) => ({ + id: variable.id, + key: variable.key, + value: variable.value, + isSecret: variable.isSecret + })) + : [emptyBuildEnvVariable()]; + setBuildEnvVariables(fallbackVariables); + loadComponentBuildEnv(component.id) + .then(({ variables }) => { + setBuildEnvVariables(variables.length + ? variables.map((variable) => ({ + id: variable.id, + key: variable.key, + value: variable.value, + isSecret: variable.isSecret + })) + : [emptyBuildEnvVariable()]); + setBuildEnvStatus('idle'); + }) + .catch((error: Error) => { + setBuildEnvStatus('error'); + toast({ type: 'error', title: '빌드 환경변수 조회 실패', message: error.message }); + }); + }; + + const closeBuildEnvModal = () => { + setBuildEnvComponent(null); + setBuildEnvVariables([emptyBuildEnvVariable()]); + setBuildEnvStatus('idle'); + }; + + const saveBuildEnv = () => { + if (!buildEnvComponent) return; + setBuildEnvStatus('saving'); + saveComponentBuildEnv(buildEnvComponent.id, buildEnvVariables) + .then(({ variables }) => { + setApiApps((current) => current.map((app) => ({ + ...app, + components: app.components?.map((component) => component.id === buildEnvComponent.id + ? { ...component, buildVariables: variables } + : component) + }))); + toast({ type: 'success', title: '빌드 환경변수 저장', message: '구성요소 빌드 환경변수를 저장했습니다.' }); + closeBuildEnvModal(); + }) + .catch((error: Error) => { + setBuildEnvStatus('error'); + toast({ type: 'error', title: '빌드 환경변수 저장 실패', message: error.message }); + }); + }; + const deleteComponent = () => { if (!deletingComponent) return; @@ -873,10 +981,13 @@ export function AppManagementPage() { {component.repository || (component.imageSource === 'EXTERNAL' ? '외부 이미지' : '미연결')} -
+
+ {!running ? ( +
+ ))} + +
+ )} + {buildEnvStatus === 'error' ?

빌드 환경변수 처리에 실패했습니다.

: null} + +
+ + +
+ + + ) : null} + {deletingComponent ? (
diff --git a/src/pages/runtime/RuntimeConfigPage.tsx b/src/pages/runtime/RuntimeConfigPage.tsx index 2210dd5..c2817b6 100644 --- a/src/pages/runtime/RuntimeConfigPage.tsx +++ b/src/pages/runtime/RuntimeConfigPage.tsx @@ -92,6 +92,7 @@ export function RuntimeConfigPage() { const [ingressHost, setIngressHost] = useState(''); const [ingressPath, setIngressPath] = useState('/'); const [ingressPort, setIngressPort] = useState(''); + const [ingressStripPrefix, setIngressStripPrefix] = useState(false); const selectedApp = apps.find((app) => app.id === appId); const selectedProfile = profiles.find((profile) => profile.id === profileId); @@ -237,11 +238,13 @@ export function RuntimeConfigPage() { host: ingressHost.trim(), path: ingressPath.trim() || '/', servicePort: Number(ingressPort), + stripPrefix: ingressStripPrefix, tlsEnabled: false }) .then(() => { setIngressHost(''); setIngressPath('/'); + setIngressStripPrefix(false); refreshProfiles(appId, selectedProfile.id); }) .catch((createError: Error) => setError(createError.message)); @@ -529,19 +532,20 @@ export function RuntimeConfigPage() { 총 {ingressRules.length}개
- +
+ {ingressRules.length === 0 ? ( - + ) : null} {ingressRules.map((rule) => ( @@ -554,6 +558,7 @@ export function RuntimeConfigPage() { +
구성요소 도메인 Path 포트Prefix 관리
등록된 접속 설정이 없습니다.
등록된 접속 설정이 없습니다.
{rule.host} {rule.path} {rule.servicePort}{rule.stripPrefix ? '제거' : '유지'}