split launcher versions into client and backend deployment
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+1
-1
@@ -483,7 +483,7 @@ function Sidebar({
|
||||
const isParentActive = hasChildren
|
||||
? item.children.some((child) => resolveNavView(child) === activeView)
|
||||
: activeView === resolveNavView(item);
|
||||
const isMenuOpen = Boolean(normalizedMenuSearch) || openMenus[item.label] || isParentActive;
|
||||
const isMenuOpen = Boolean(normalizedMenuSearch) || (openMenus[item.label] ?? isParentActive);
|
||||
|
||||
return (
|
||||
<div key={item.label}>
|
||||
|
||||
@@ -269,7 +269,7 @@ export function LauncherBuildPage() {
|
||||
<div className="rounded-md border border-border bg-background p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">클라이언트 빌드 환경변수</p>
|
||||
<p className="text-xs font-medium text-foreground">클라이언트 빌드 환경변수</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">저장 후 실행한 빌드부터 반영됩니다.</p>
|
||||
</div>
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={persistBuildEnv} disabled={buildEnvSaving}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Plus, RotateCcw } from 'lucide-react';
|
||||
import { Play, Plus, RotateCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
@@ -6,8 +6,12 @@ import { PageTitle } from '../../layout/AppLayout';
|
||||
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
|
||||
import {
|
||||
createLauncherVersion,
|
||||
deployLauncherBackend,
|
||||
getLauncherBackendDeployment,
|
||||
listLauncherBuildJobs,
|
||||
listLauncherVersions,
|
||||
type LauncherBackendDeployment,
|
||||
type LauncherBackendDeployResult,
|
||||
type LauncherBuildJob,
|
||||
type LauncherVersion
|
||||
} from './launcher-api';
|
||||
@@ -22,11 +26,16 @@ function displayValue(value?: string | null) {
|
||||
|
||||
export function LauncherVersionsPage() {
|
||||
const { toast } = useToast();
|
||||
const [tab, setTab] = useState<'client' | 'backend'>('client');
|
||||
const [versions, setVersions] = useState<LauncherVersion[]>([]);
|
||||
const [buildJobs, setBuildJobs] = useState<LauncherBuildJob[]>([]);
|
||||
const [backendDeployment, setBackendDeployment] = useState<LauncherBackendDeployment | null>(null);
|
||||
const [deployResult, setDeployResult] = useState<LauncherBackendDeployResult | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [releaseOpen, setReleaseOpen] = useState(false);
|
||||
const [deployOpen, setDeployOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deploying, setDeploying] = useState(false);
|
||||
const [releaseForm, setReleaseForm] = useState({
|
||||
buildJobId: '',
|
||||
version: '',
|
||||
@@ -36,7 +45,15 @@ export function LauncherVersionsPage() {
|
||||
});
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
const launcher = useMemo(() => versions.find((item) => item.isActive) ?? versions[0], [versions]);
|
||||
const successfulBuildJobs = useMemo(() => buildJobs.filter((job) => job.status === 'SUCCEEDED'), [buildJobs]);
|
||||
const successfulBuildJobs = useMemo(
|
||||
() => buildJobs.filter((job) => job.target !== 'launcher-backend' && job.status === 'SUCCEEDED'),
|
||||
[buildJobs]
|
||||
);
|
||||
const successfulBackendBuildJobs = useMemo(
|
||||
() => buildJobs.filter((job) => job.target === 'launcher-backend' && job.status === 'SUCCEEDED'),
|
||||
[buildJobs]
|
||||
);
|
||||
const latestBackendBuild = backendDeployment?.latestBuild ?? successfulBackendBuildJobs[0] ?? null;
|
||||
const selectedBuildJob = useMemo(
|
||||
() => successfulBuildJobs.find((job) => job.id === releaseForm.buildJobId),
|
||||
[releaseForm.buildJobId, successfulBuildJobs]
|
||||
@@ -45,10 +62,11 @@ export function LauncherVersionsPage() {
|
||||
|
||||
const refresh = () => {
|
||||
setLoading(true);
|
||||
Promise.all([listLauncherVersions(), listLauncherBuildJobs()])
|
||||
.then(([nextVersions, nextBuildJobs]) => {
|
||||
Promise.all([listLauncherVersions(), listLauncherBuildJobs(), getLauncherBackendDeployment()])
|
||||
.then(([nextVersions, nextBuildJobs, nextBackendDeployment]) => {
|
||||
setVersions(nextVersions);
|
||||
setBuildJobs(nextBuildJobs);
|
||||
setBackendDeployment(nextBackendDeployment);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast({ type: 'error', title: '런처 버전 조회 실패', message: error.message });
|
||||
@@ -107,29 +125,61 @@ export function LauncherVersionsPage() {
|
||||
.finally(() => setSaving(false));
|
||||
};
|
||||
|
||||
const deployBackend = () => {
|
||||
if (!latestBackendBuild?.downloadUrl) return;
|
||||
setDeploying(true);
|
||||
deployLauncherBackend(latestBackendBuild.downloadUrl)
|
||||
.then((result) => {
|
||||
setDeployResult(result);
|
||||
toast({ type: 'success', message: '런처 백엔드를 배포했습니다.' });
|
||||
setDeployOpen(false);
|
||||
refresh();
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast({ type: 'error', title: '백엔드 배포 실패', message: error.message });
|
||||
})
|
||||
.finally(() => setDeploying(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
eyebrow="런처 운영"
|
||||
title="버전 관리"
|
||||
description="스튜디오 메인 런처의 버전과 배포 파일 정보를 관리합니다."
|
||||
description="스튜디오 메인 런처 클라이언트 릴리즈와 백엔드 배포를 관리합니다."
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={refresh} disabled={loading}>
|
||||
<RotateCcw className={loading ? 'size-4 animate-spin' : 'size-4'} />
|
||||
새로고침
|
||||
</Button>
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={openRelease}>
|
||||
<Plus className="size-4" />
|
||||
릴리즈 등록
|
||||
</Button>
|
||||
{tab === 'client' ? (
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={openRelease}>
|
||||
<Plus className="size-4" />
|
||||
릴리즈 등록
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={() => setDeployOpen(true)} disabled={!latestBackendBuild?.downloadUrl}>
|
||||
<Play className="size-4" />
|
||||
백엔드 배포
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{!loading && !launcher ? <Card className="px-5 py-12 text-center text-sm text-muted-foreground">등록된 런처 버전이 없습니다.</Card> : null}
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
<Button variant={tab === 'client' ? 'default' : 'outline'} onClick={() => setTab('client')}>
|
||||
클라이언트 릴리즈
|
||||
</Button>
|
||||
<Button variant={tab === 'backend' ? 'default' : 'outline'} onClick={() => setTab('backend')}>
|
||||
백엔드 배포
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{launcher ? (
|
||||
{tab === 'client' && !loading && !launcher ? <Card className="px-5 py-12 text-center text-sm text-muted-foreground">등록된 런처 버전이 없습니다.</Card> : null}
|
||||
|
||||
{tab === 'client' && launcher ? (
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(360px,0.65fr)]">
|
||||
<Card>
|
||||
<CardHeader title="메인 런처 배포 상태" description="현재 배포 기준으로 사용하는 런처 정보입니다." />
|
||||
@@ -183,6 +233,59 @@ export function LauncherVersionsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tab === 'backend' ? (
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(360px,0.75fr)]">
|
||||
<Card>
|
||||
<CardHeader title="런처 백엔드 배포 상태" description="현재 k3s에서 실행 중인 런처 백엔드 이미지와 최신 성공 빌드입니다." />
|
||||
<div className="space-y-4 p-5">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">Namespace</p>
|
||||
<p className="mt-2 font-mono text-sm font-medium">{backendDeployment?.namespace || 'studio-system'}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">Deployment</p>
|
||||
<p className="mt-2 font-mono text-sm font-medium">{backendDeployment?.deployment || 'studio-launcher-be'}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">상태</p>
|
||||
<div className="mt-2">
|
||||
<Badge tone={backendDeployment?.available ? 'green' : 'red'}>{backendDeployment?.available ? '조회됨' : '조회 실패'}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">현재 배포 이미지</p>
|
||||
<p className="mt-2 break-all font-mono text-sm font-medium">{backendDeployment?.currentImage || '-'}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">최신 성공 빌드 이미지</p>
|
||||
<p className="mt-2 break-all font-mono text-sm font-medium">{latestBackendBuild?.downloadUrl || '-'}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>버전 <b className="text-foreground">{latestBackendBuild?.version || '-'}</b></span>
|
||||
<span>브랜치 <b className="text-foreground">{latestBackendBuild?.branch || '-'}</b></span>
|
||||
<span>커밋 <b className="font-mono text-foreground">{latestBackendBuild?.commitSha || '-'}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => setDeployOpen(true)} disabled={!latestBackendBuild?.downloadUrl || deploying}>
|
||||
<Play className="size-4" />
|
||||
백엔드 배포
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader title="배포 로그" description="마지막 백엔드 배포 실행 결과입니다." />
|
||||
<div className="p-5">
|
||||
<pre className="min-h-64 overflow-auto rounded-md border border-neutral-800 bg-neutral-950 p-4 font-mono text-xs leading-6 text-neutral-100">
|
||||
{deployResult?.logs || '아직 이 화면에서 실행한 배포 로그가 없습니다.'}
|
||||
</pre>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Modal
|
||||
open={releaseOpen}
|
||||
title="릴리즈 등록"
|
||||
@@ -237,6 +340,28 @@ export function LauncherVersionsPage() {
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={deployOpen}
|
||||
title="런처 백엔드 배포"
|
||||
onClose={() => setDeployOpen(false)}
|
||||
footer={<><Button variant="outline" onClick={() => setDeployOpen(false)}>취소</Button><Button onClick={deployBackend} disabled={deploying || !latestBackendBuild?.downloadUrl}>{deploying ? '배포 중' : '배포'}</Button></>}
|
||||
>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="rounded-md border border-border bg-muted/20 px-4 py-3">
|
||||
<p className="font-medium">최신 성공 빌드 이미지로 런처 백엔드를 배포합니다.</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">Deployment 이미지가 교체되고 rollout 완료까지 대기합니다.</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">배포 이미지</p>
|
||||
<p className="mt-2 break-all font-mono text-xs font-medium">{latestBackendBuild?.downloadUrl || '-'}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background p-4">
|
||||
<p className="text-xs text-muted-foreground">현재 이미지</p>
|
||||
<p className="mt-2 break-all font-mono text-xs text-muted-foreground">{backendDeployment?.currentImage || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<LoadingModal open={loadingVisible} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,25 @@ export type LauncherBuildJob = {
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type LauncherBackendDeployment = {
|
||||
namespace: string;
|
||||
deployment: string;
|
||||
container: string;
|
||||
currentImage?: string | null;
|
||||
available: boolean;
|
||||
source?: LauncherSource['backend'];
|
||||
latestBuild?: LauncherBuildJob | null;
|
||||
};
|
||||
|
||||
export type LauncherBackendDeployResult = {
|
||||
deployed: boolean;
|
||||
namespace: string;
|
||||
deployment: string;
|
||||
container: string;
|
||||
image: string;
|
||||
logs: string;
|
||||
};
|
||||
|
||||
export type LauncherBuildEnvVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
@@ -199,6 +218,17 @@ export function listLauncherBuildJobs() {
|
||||
return apiRequest<LauncherBuildJob[]>('/launcher/builds');
|
||||
}
|
||||
|
||||
export function getLauncherBackendDeployment() {
|
||||
return apiRequest<LauncherBackendDeployment>('/launcher/backend-deployment');
|
||||
}
|
||||
|
||||
export function deployLauncherBackend(image: string) {
|
||||
return apiRequest<LauncherBackendDeployResult>('/launcher/backend-deployment', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ image })
|
||||
});
|
||||
}
|
||||
|
||||
export function listLauncherBuildEnv() {
|
||||
return apiRequest<{ variables: LauncherBuildEnvVariable[] }>('/launcher/build-env');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user