This commit is contained in:
@@ -5,20 +5,28 @@ import { useToast } from '../../components/toast';
|
||||
import { PageTitle } from '../../layout/AppLayout';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
|
||||
import { listCommonAppPipelines, type CommonAppPipeline } from './common-app-api';
|
||||
import { listCommonAppPipelineHistory, listCommonAppPipelines, type CommonAppPipeline } from './common-app-api';
|
||||
import { formatDuration, formatPipelineRelativeTime, formatRepositoryName, formatShortSha, isActivePipelineStatus, pipelineStatusDotClass, pipelineStatusTone } from './common-app-utils';
|
||||
|
||||
const historyPageSize = 5;
|
||||
|
||||
export function CommonAppBuildPage() {
|
||||
const { toast } = useToast();
|
||||
const [pipelines, setPipelines] = useState<CommonAppPipeline[]>([]);
|
||||
const [selectedRepoId, setSelectedRepoId] = useState('');
|
||||
const [history, setHistory] = useState<CommonAppPipeline[]>([]);
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
const [hasNextHistoryPage, setHasNextHistoryPage] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [lastLoadedAt, setLastLoadedAt] = useState<number | null>(null);
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
const loadingVisible = useMinimumLoading(loading || historyLoading);
|
||||
|
||||
const runningCount = useMemo(
|
||||
() => pipelines.filter((pipeline) => isActivePipelineStatus(pipeline.status)).length,
|
||||
[pipelines]
|
||||
);
|
||||
const selectedPipeline = pipelines.find((pipeline) => pipeline.repoId === selectedRepoId) ?? pipelines[0];
|
||||
|
||||
const refresh = (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
@@ -26,6 +34,10 @@ export function CommonAppBuildPage() {
|
||||
.then((nextPipelines) => {
|
||||
setPipelines(nextPipelines);
|
||||
setLastLoadedAt(Date.now());
|
||||
setSelectedRepoId((current) => {
|
||||
if (current && nextPipelines.some((pipeline) => pipeline.repoId === current)) return current;
|
||||
return nextPipelines.find((pipeline) => pipeline.repoId)?.repoId ?? '';
|
||||
});
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (!silent) toast({ type: 'error', title: '공통앱 빌드 조회 실패', message: error.message });
|
||||
@@ -44,6 +56,40 @@ export function CommonAppBuildPage() {
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setHistoryPage(1);
|
||||
}, [selectedRepoId]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
if (!selectedRepoId) {
|
||||
setHistory([]);
|
||||
setHasNextHistoryPage(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setHistoryLoading(true);
|
||||
listCommonAppPipelineHistory(selectedRepoId, historyPage, historyPageSize)
|
||||
.then((result) => {
|
||||
if (canceled) return;
|
||||
setHistory(result.pipelines ?? []);
|
||||
setHasNextHistoryPage(Boolean(result.hasNext));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (canceled) return;
|
||||
setHistory([]);
|
||||
setHasNextHistoryPage(false);
|
||||
toast({ type: 'error', title: '파이프라인 이력 조회 실패', message: error.message });
|
||||
})
|
||||
.finally(() => {
|
||||
if (!canceled) setHistoryLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [historyPage, selectedRepoId, toast]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -58,6 +104,7 @@ export function CommonAppBuildPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(320px,0.75fr)_minmax(0,1.25fr)]">
|
||||
<Card className="min-w-0 overflow-hidden">
|
||||
<CardHeader
|
||||
title="모든 저장소"
|
||||
@@ -76,14 +123,48 @@ export function CommonAppBuildPage() {
|
||||
<div className="flex min-h-40 items-center justify-center px-5 text-sm text-muted-foreground">표시할 저장소가 없습니다.</div>
|
||||
) : null}
|
||||
<div className="divide-y divide-border">
|
||||
{pipelines.map((pipeline) => (
|
||||
<div key={pipeline.id} className="flex flex-col gap-3 px-5 py-3 transition-colors hover:bg-muted/20 md:flex-row md:items-center md:justify-between">
|
||||
<div className="min-w-0">
|
||||
{pipelines.map((pipeline) => {
|
||||
const selected = pipeline.repoId === selectedRepoId;
|
||||
return (
|
||||
<button
|
||||
key={pipeline.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'block w-full px-5 py-3 text-left transition-colors hover:bg-muted/20',
|
||||
selected && 'bg-primary/20 shadow-[inset_4px_0_0_var(--color-primary)] hover:bg-primary/25'
|
||||
)}
|
||||
onClick={() => pipeline.repoId && setSelectedRepoId(pipeline.repoId)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className={cn('size-2.5 shrink-0 rounded-full', pipelineStatusDotClass(pipeline.status))} />
|
||||
<p className="truncate text-sm font-semibold">{formatRepositoryName(pipeline.repository)}</p>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-sm text-muted-foreground">{pipeline.message || '-'}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="min-w-0 overflow-hidden">
|
||||
<CardHeader
|
||||
title="파이프라인 이력"
|
||||
description={selectedPipeline ? formatRepositoryName(selectedPipeline.repository) : '저장소를 선택하세요'}
|
||||
action={<Badge tone="neutral">5개씩</Badge>}
|
||||
/>
|
||||
<div className="min-h-[430px] divide-y divide-border">
|
||||
{history.length === 0 && !historyLoading ? (
|
||||
<div className="flex min-h-40 items-center justify-center px-5 text-sm text-muted-foreground">표시할 파이프라인 이력이 없습니다.</div>
|
||||
) : null}
|
||||
{history.map((pipeline) => (
|
||||
<div key={pipeline.id} className="flex flex-col gap-3 px-5 py-3 transition-colors hover:bg-muted/20 md:flex-row md:items-center md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className={cn('size-2.5 shrink-0 rounded-full', pipelineStatusDotClass(pipeline.status))} />
|
||||
<span className="shrink-0 font-mono text-xs text-muted-foreground">#{pipeline.number ?? pipeline.id}</span>
|
||||
<p className="truncate text-sm font-semibold">{pipeline.message || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2 text-xs text-muted-foreground md:justify-end">
|
||||
<Badge tone={pipelineStatusTone(pipeline.status)}>
|
||||
@@ -100,8 +181,19 @@ export function CommonAppBuildPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t border-border px-5 py-3 text-xs text-muted-foreground">
|
||||
<span>현재 {historyPage}페이지</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" disabled={historyLoading || historyPage <= 1} onClick={() => setHistoryPage((page) => Math.max(1, page - 1))}>
|
||||
이전
|
||||
</Button>
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" disabled={historyLoading || !hasNextHistoryPage} onClick={() => setHistoryPage((page) => page + 1)}>
|
||||
다음
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<LoadingModal open={loadingVisible} />
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,9 @@ import { apiRequest } from '../../lib/api';
|
||||
|
||||
export type CommonAppPipeline = {
|
||||
id: string;
|
||||
repoId?: string | null;
|
||||
repository: string;
|
||||
number?: number | null;
|
||||
message: string;
|
||||
branch: string;
|
||||
commitSha?: string | null;
|
||||
@@ -17,3 +19,16 @@ export function listCommonAppPipelines() {
|
||||
return apiRequest<{ repos?: CommonAppPipeline[]; baseUrl?: string | null }>('/woodpecker/repos')
|
||||
.then((result) => (result.repos ?? []).map((repo) => ({ ...repo, ciUrl: repo.ciUrl ?? result.baseUrl ?? null })));
|
||||
}
|
||||
|
||||
export function listCommonAppPipelineHistory(repoId: string, page = 1, limit = 5) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
limit: String(limit)
|
||||
});
|
||||
return apiRequest<{
|
||||
page: number;
|
||||
pageSize: number;
|
||||
hasNext: boolean;
|
||||
pipelines: CommonAppPipeline[];
|
||||
}>(`/woodpecker/repos/${encodeURIComponent(repoId)}/pipelines?${params.toString()}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user