improve admin release and deployment safeguards
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -7,7 +7,7 @@ import type { BuildJob, SourceComponent } from '../../types/application';
|
||||
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
|
||||
import { AppSelector, StatusBadge, formatDate, isBuildDone, useApps } from './ApplicationControls';
|
||||
import { cancelBuildJob, createBuildJob, deleteBuildJob, listBuildJobsPage, listSources } from './application-api';
|
||||
import { getStoredSelectedComponentId, setStoredSelectedComponentId } from '../../lib/selected-app';
|
||||
import { getStoredSelectedComponentId, selectedComponentChangedEventName, setStoredSelectedComponentId } from '../../lib/selected-app';
|
||||
|
||||
function blockPageAgentDestructiveClick(event: MouseEvent<HTMLElement>) {
|
||||
if (event.nativeEvent.isTrusted) return false;
|
||||
@@ -93,8 +93,10 @@ export function BuildManagementPage() {
|
||||
const [branch, setBranch] = useState('develop');
|
||||
const [commitSha, setCommitSha] = useState('');
|
||||
const [selectedJob, setSelectedJob] = useState<BuildJob | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<BuildJob | null>(null);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deletingJob, setDeletingJob] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [range, setRange] = useState<BuildRange>('week');
|
||||
const [totalJobs, setTotalJobs] = useState(0);
|
||||
@@ -199,6 +201,27 @@ export function BuildManagementPage() {
|
||||
}
|
||||
}, [componentOptions, selectedAppId, selectedComponentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedComponentId = getStoredSelectedComponentId(selectedAppId);
|
||||
if (!storedComponentId || storedComponentId === selectedComponentId) return;
|
||||
if (!componentOptions.some((source) => source.id === storedComponentId)) return;
|
||||
setSelectedComponentId(storedComponentId);
|
||||
setBranch('develop');
|
||||
}, [componentOptions, selectedAppId, selectedComponentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelectedComponentChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ appId?: string; componentId?: string }>).detail;
|
||||
if (detail?.appId !== selectedAppId || !detail.componentId) return;
|
||||
if (!componentOptions.some((source) => source.id === detail.componentId)) return;
|
||||
setSelectedComponentId(detail.componentId);
|
||||
setBranch('develop');
|
||||
};
|
||||
|
||||
window.addEventListener(selectedComponentChangedEventName, handleSelectedComponentChange);
|
||||
return () => window.removeEventListener(selectedComponentChangedEventName, handleSelectedComponentChange);
|
||||
}, [componentOptions, selectedAppId]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [selectedAppId, range]);
|
||||
@@ -237,13 +260,18 @@ export function BuildManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const removeJob = async (id: string) => {
|
||||
const removeJob = async () => {
|
||||
if (!deleteTarget || deletingJob) return;
|
||||
setDeletingJob(true);
|
||||
try {
|
||||
await deleteBuildJob(id);
|
||||
await deleteBuildJob(deleteTarget.id);
|
||||
toast({ type: 'success', message: '빌드 작업이 삭제되었습니다.' });
|
||||
setDeleteTarget(null);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast({ type: 'error', message: error instanceof Error ? error.message : '삭제에 실패했습니다.' });
|
||||
} finally {
|
||||
setDeletingJob(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -376,13 +404,13 @@ export function BuildManagementPage() {
|
||||
)}
|
||||
{isBuildDone(job.status) ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8 px-0"
|
||||
variant="ghost"
|
||||
className="size-8 px-0 text-rose-300 hover:text-rose-200"
|
||||
title="삭제"
|
||||
aria-label="삭제"
|
||||
onClick={(event) => {
|
||||
if (blockPageAgentDestructiveClick(event)) return;
|
||||
removeJob(job.id);
|
||||
setDeleteTarget(job);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
@@ -451,6 +479,31 @@ export function BuildManagementPage() {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<Modal
|
||||
open={Boolean(deleteTarget)}
|
||||
title="빌드 작업 삭제"
|
||||
onClose={() => {
|
||||
if (!deletingJob) setDeleteTarget(null);
|
||||
}}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" disabled={deletingJob} onClick={() => setDeleteTarget(null)}>취소</Button>
|
||||
<Button className="bg-rose-600 text-white hover:bg-rose-500" disabled={deletingJob} onClick={removeJob}>
|
||||
{deletingJob ? '삭제 중' : '삭제'}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
<span className="font-semibold text-foreground">{deleteTarget?.component.app.name ?? '-'}</span>
|
||||
{' / '}
|
||||
<span className="font-semibold text-foreground">{deleteTarget?.component.name ?? '-'}</span>
|
||||
{' '}빌드 작업을 삭제합니다.
|
||||
</p>
|
||||
<p className="text-muted-foreground">삭제된 빌드 작업 이력과 로그는 목록에서 사라집니다.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={confirmOpen}
|
||||
title="빌드 실행 확인"
|
||||
|
||||
Reference in New Issue
Block a user