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'; 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'; function artifactName(version: string) { return `autonomy-studio-launcher-${version || '0.0.0'}.deb`; } function displayValue(value?: string | null) { return value?.trim() || '-'; } export function LauncherVersionsPage() { const { toast } = useToast(); const [tab, setTab] = useState<'client' | 'backend'>('client'); const [versions, setVersions] = useState([]); const [buildJobs, setBuildJobs] = useState([]); const [backendDeployment, setBackendDeployment] = useState(null); const [deployResult, setDeployResult] = useState(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: '', minimumVersion: '', downloadUrl: '', releaseNotes: '' }); const loadingVisible = useMinimumLoading(loading); const launcher = useMemo(() => versions.find((item) => item.isActive) ?? versions[0], [versions]); 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] ); const currentArtifact = selectedBuildJob?.artifact || artifactName(releaseForm.version); const refresh = () => { setLoading(true); 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 }); }) .finally(() => setLoading(false)); }; useEffect(() => { refresh(); }, []); const openRelease = () => { const latestBuild = successfulBuildJobs[0]; setReleaseForm({ buildJobId: latestBuild?.id ?? '', version: latestBuild?.version ?? '', minimumVersion: launcher?.minimumVersion ?? '', downloadUrl: latestBuild?.downloadUrl ?? '', releaseNotes: '' }); setReleaseOpen(true); }; const selectBuildJob = (buildJobId: string) => { const buildJob = successfulBuildJobs.find((job) => job.id === buildJobId); setReleaseForm((prev) => ({ ...prev, buildJobId, version: buildJob?.version ?? prev.version, downloadUrl: buildJob?.downloadUrl ?? prev.downloadUrl })); }; const saveRelease = (event: FormEvent) => { event.preventDefault(); if (!selectedBuildJob || !releaseForm.version.trim() || !releaseForm.downloadUrl.trim()) return; setSaving(true); createLauncherVersion({ version: releaseForm.version.trim(), minimumVersion: releaseForm.minimumVersion.trim() || undefined, artifact: currentArtifact, downloadUrl: releaseForm.downloadUrl.trim(), releaseNotes: releaseForm.releaseNotes.trim() || undefined, repository: selectedBuildJob?.repository ?? launcher?.repository ?? undefined, branch: selectedBuildJob?.branch ?? launcher?.branch ?? 'release', commitSha: selectedBuildJob?.commitSha ?? launcher?.commitSha ?? undefined }) .then(() => { toast({ type: 'success', message: '릴리즈를 등록했습니다.' }); setReleaseOpen(false); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: '릴리즈 등록 실패', message: error.message }); }) .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 ( <> {tab === 'client' ? ( ) : ( )} } />
{tab === 'client' && !loading && !launcher ? 등록된 런처 버전이 없습니다. : null} {tab === 'client' && launcher ? (

Autonomy Studio Launcher

{launcher.isActive ? '활성' : '비활성'}

최신 {launcher.version} · 최소 지원 {launcher.minimumVersion || '-'}

배포 파일

{launcher.artifact}

다운로드 URL

{launcher.downloadUrl || '-'}

소스 기준

Repository

{launcher.repository || '-'}

Branch

{launcher.branch || '-'}

Commit SHA

{launcher.commitSha || '-'}

{launcher.releaseNotes || '-'}

) : null} {tab === 'backend' ? (

Namespace

{backendDeployment?.namespace || 'studio-system'}

Deployment

{backendDeployment?.deployment || 'studio-launcher-be'}

상태

{backendDeployment?.available ? '조회됨' : '조회 실패'}

현재 배포 이미지

{backendDeployment?.currentImage || '-'}

최신 성공 빌드 이미지

{latestBackendBuild?.downloadUrl || '-'}

버전 {latestBackendBuild?.version || '-'} 브랜치 {latestBackendBuild?.branch || '-'} 커밋 {latestBackendBuild?.commitSha || '-'}
                {deployResult?.logs || '아직 이 화면에서 실행한 배포 로그가 없습니다.'}
              
) : null} setReleaseOpen(false)} className="!max-w-3xl" footer={<>} >

배포 파일 기준 등록

성공한 빌드 결과의 버전과 다운로드 URL을 런처 배포 기준으로 저장합니다.

버전 *
{displayValue(releaseForm.version)}

배포 파일

{displayValue(currentArtifact)}

다운로드 URL *

{displayValue(releaseForm.downloadUrl)}