improve admin release and deployment safeguards
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 09:46:36 +09:00
parent fd590540bc
commit f67017623f
9 changed files with 394 additions and 45 deletions
@@ -1,12 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal, Plus, RotateCcw } from 'lucide-react';
import { ChevronLeft, ChevronRight, MoreHorizontal, Plus, RotateCcw, Trash2 } from 'lucide-react';
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
import { useToast } from '../../components/toast';
import { PageTitle } from '../../layout/AppLayout';
import type { AppRelease, DeploymentJob } from '../../types/application';
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
import { AppSelector, formatDate, useApps } from './ApplicationControls';
import { createDeployment, listDeploymentsPage, listReleases } from './application-api';
import { createDeployment, deleteRelease, listDeploymentsPage, listReleases } from './application-api';
function statusLabel(status: string) {
return {
@@ -79,8 +79,10 @@ export function DeploymentManagementPage() {
const [updateLauncherAppVersion, setUpdateLauncherAppVersion] = useState(true);
const [deployModalOpen, setDeployModalOpen] = useState(false);
const [selectedDeployment, setSelectedDeployment] = useState<DeploymentJob | null>(null);
const [deleteTarget, setDeleteTarget] = useState<AppRelease | null>(null);
const [loading, setLoading] = useState(true);
const [deploying, setDeploying] = useState(false);
const [deletingRelease, setDeletingRelease] = useState(false);
const [page, setPage] = useState(1);
const [range, setRange] = useState<DeploymentRange>('week');
const [totalDeployments, setTotalDeployments] = useState(0);
@@ -175,6 +177,21 @@ export function DeploymentManagementPage() {
}
};
const removeRelease = async () => {
if (!deleteTarget || deletingRelease) return;
setDeletingRelease(true);
try {
await deleteRelease(deleteTarget.id);
toast({ type: 'success', message: '릴리즈를 삭제했습니다.' });
setDeleteTarget(null);
await refresh();
} catch (error) {
toast({ type: 'error', message: error instanceof Error ? error.message : '릴리즈 삭제에 실패했습니다.' });
} finally {
setDeletingRelease(false);
}
};
return (
<>
<PageTitle
@@ -298,18 +315,29 @@ export function DeploymentManagementPage() {
</div>
) : null}
{deployableCards.map((release) => (
<button
type="button"
<div
key={release.id}
className="w-full rounded-md border border-border bg-background px-3 py-3 text-left transition-colors hover:bg-muted/20"
onClick={() => openDeployModal(release)}
className="rounded-md border border-border bg-background px-3 py-3 transition-colors hover:bg-muted/20"
>
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium">{release.app.name}</span>
<Badge tone="green">{release.version}</Badge>
<div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => openDeployModal(release)}>
<div className="flex items-center gap-2">
<span className="truncate text-sm font-medium">{release.app.name}</span>
<Badge tone="green">{release.version}</Badge>
</div>
<p className="mt-2 truncate font-mono text-xs text-muted-foreground">{firstArtifact(release)}</p>
</button>
<Button
variant="ghost"
className="size-8 shrink-0 px-0 text-rose-300 hover:text-rose-200"
title="릴리즈 삭제"
aria-label={`${release.app.name} ${release.version} 릴리즈 삭제`}
onClick={() => setDeleteTarget(release)}
>
<Trash2 className="size-4" />
</Button>
</div>
<p className="mt-2 truncate font-mono text-xs text-muted-foreground">{firstArtifact(release)}</p>
</button>
</div>
))}
</div>
</Card>
@@ -358,6 +386,34 @@ export function DeploymentManagementPage() {
</div>
)}
</Modal>
<Modal
open={Boolean(deleteTarget)}
title="릴리즈 삭제"
onClose={() => {
if (!deletingRelease) setDeleteTarget(null);
}}
footer={(
<>
<Button variant="outline" disabled={deletingRelease} onClick={() => setDeleteTarget(null)}>취소</Button>
<Button className="bg-rose-600 text-white hover:bg-rose-500" disabled={deletingRelease} onClick={removeRelease}>
{deletingRelease ? '삭제 중' : '삭제'}
</Button>
</>
)}
>
<div className="space-y-3 text-sm">
<p>
<span className="font-semibold text-foreground">{deleteTarget?.app.name ?? '-'}</span>
{' '}
<span className="font-semibold text-foreground">{deleteTarget?.version ?? '-'}</span>
{' '}릴리즈를 삭제합니다.
</p>
<p className="text-muted-foreground">
이미 성공 배포된 릴리즈는 서버에서 삭제가 차단됩니다.
</p>
</div>
</Modal>
<LoadingModal open={loadingVisible} />
</>
);