This commit is contained in:
+20
-6
@@ -865,6 +865,17 @@ function isRuntimeResourceHealthy(resource: DashboardRuntimeResource) {
|
|||||||
return Number.isFinite(ready) && Number.isFinite(total) && total > 0 && ready === total;
|
return Number.isFinite(ready) && Number.isFinite(total) && total > 0 && ready === total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isIgnoredRuntimeResource(resource: DashboardRuntimeResource) {
|
||||||
|
const normalizedName = resource.name.toLowerCase();
|
||||||
|
const normalizedStatus = resource.status.toLowerCase();
|
||||||
|
if (resource.kind === 'Pod' && normalizedStatus === 'completed') return true;
|
||||||
|
if (resource.kind === 'Job' && ['completed', 'succeeded', 'success'].includes(normalizedStatus)) return true;
|
||||||
|
if ((resource.kind === 'Pod' || resource.kind === 'Job') && normalizedName.includes('migration') && ['completed', 'succeeded', 'success'].includes(normalizedStatus)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function formatJobStatus(status?: string | null) {
|
function formatJobStatus(status?: string | null) {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
PENDING: '대기',
|
PENDING: '대기',
|
||||||
@@ -905,8 +916,9 @@ function MetricCards({
|
|||||||
const activeApps = dashboardApps.filter((app) => isActiveStatus(app.status)).length;
|
const activeApps = dashboardApps.filter((app) => isActiveStatus(app.status)).length;
|
||||||
const activeSessions = sessions.filter((session) => session.state === '활성').length;
|
const activeSessions = sessions.filter((session) => session.state === '활성').length;
|
||||||
const activeWorkTotal = activeWork.builds + activeWork.releases + activeWork.deployments;
|
const activeWorkTotal = activeWork.builds + activeWork.releases + activeWork.deployments;
|
||||||
const healthyRuntimeCount = runtimeResources.filter(isRuntimeResourceHealthy).length;
|
const countedRuntimeResources = runtimeResources.filter((resource) => !isIgnoredRuntimeResource(resource));
|
||||||
const runtimeRate = runtimeResources.length ? Math.round((healthyRuntimeCount / runtimeResources.length) * 100) : null;
|
const healthyRuntimeCount = countedRuntimeResources.filter(isRuntimeResourceHealthy).length;
|
||||||
|
const runtimeRate = countedRuntimeResources.length ? Math.round((healthyRuntimeCount / countedRuntimeResources.length) * 100) : null;
|
||||||
const cards = [
|
const cards = [
|
||||||
{
|
{
|
||||||
label: '활성 앱',
|
label: '활성 앱',
|
||||||
@@ -932,8 +944,8 @@ function MetricCards({
|
|||||||
{
|
{
|
||||||
label: '런타임 정상률',
|
label: '런타임 정상률',
|
||||||
value: loading || runtimeRate === null ? '-' : `${runtimeRate}%`,
|
value: loading || runtimeRate === null ? '-' : `${runtimeRate}%`,
|
||||||
delta: runtimeResources.length ? `${healthyRuntimeCount}/${runtimeResources.length}` : '리소스 없음',
|
delta: countedRuntimeResources.length ? `${healthyRuntimeCount}/${countedRuntimeResources.length}` : '리소스 없음',
|
||||||
hint: 'namespace 리소스 Ready 기준',
|
hint: '완료된 Job/마이그레이션 제외',
|
||||||
tone: runtimeRate !== null && runtimeRate < 80 ? 'red' as const : 'green' as const
|
tone: runtimeRate !== null && runtimeRate < 80 ? 'red' as const : 'green' as const
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -1258,6 +1270,8 @@ function AppRegistryTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ContainerPanel({ resources, loading, onOpen }: { resources: DashboardRuntimeResource[]; loading: boolean; onOpen: () => void }) {
|
function ContainerPanel({ resources, loading, onOpen }: { resources: DashboardRuntimeResource[]; loading: boolean; onOpen: () => void }) {
|
||||||
|
const visibleResources = resources.filter((resource) => !isIgnoredRuntimeResource(resource));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
@@ -1297,10 +1311,10 @@ function ContainerPanel({ resources, loading, onOpen }: { resources: DashboardRu
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : resources.length === 0 ? (
|
) : visibleResources.length === 0 ? (
|
||||||
<div className="px-5 py-12 text-center text-sm text-muted-foreground">표시할 런타임 리소스가 없습니다.</div>
|
<div className="px-5 py-12 text-center text-sm text-muted-foreground">표시할 런타임 리소스가 없습니다.</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!loading && resources.slice(0, 8).map((item) => (
|
{!loading && visibleResources.slice(0, 8).map((item) => (
|
||||||
<div key={`${item.namespace}-${item.kind}-${item.name}`} className="px-5 py-4">
|
<div key={`${item.namespace}-${item.kind}-${item.name}`} className="px-5 py-4">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export function CommonAppBuildPage() {
|
|||||||
const [selectedRepoId, setSelectedRepoId] = useState('');
|
const [selectedRepoId, setSelectedRepoId] = useState('');
|
||||||
const [history, setHistory] = useState<CommonAppPipeline[]>([]);
|
const [history, setHistory] = useState<CommonAppPipeline[]>([]);
|
||||||
const [historyPage, setHistoryPage] = useState(1);
|
const [historyPage, setHistoryPage] = useState(1);
|
||||||
|
const [historyTotal, setHistoryTotal] = useState<number | null>(null);
|
||||||
|
const [historyTotalPages, setHistoryTotalPages] = useState<number | null>(null);
|
||||||
const [hasNextHistoryPage, setHasNextHistoryPage] = useState(false);
|
const [hasNextHistoryPage, setHasNextHistoryPage] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [historyLoading, setHistoryLoading] = useState(false);
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
@@ -64,6 +66,8 @@ export function CommonAppBuildPage() {
|
|||||||
let canceled = false;
|
let canceled = false;
|
||||||
if (!selectedRepoId) {
|
if (!selectedRepoId) {
|
||||||
setHistory([]);
|
setHistory([]);
|
||||||
|
setHistoryTotal(null);
|
||||||
|
setHistoryTotalPages(null);
|
||||||
setHasNextHistoryPage(false);
|
setHasNextHistoryPage(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,11 +77,15 @@ export function CommonAppBuildPage() {
|
|||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
setHistory(result.pipelines ?? []);
|
setHistory(result.pipelines ?? []);
|
||||||
|
setHistoryTotal(result.total ?? null);
|
||||||
|
setHistoryTotalPages(result.totalPages ?? null);
|
||||||
setHasNextHistoryPage(Boolean(result.hasNext));
|
setHasNextHistoryPage(Boolean(result.hasNext));
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
setHistory([]);
|
setHistory([]);
|
||||||
|
setHistoryTotal(null);
|
||||||
|
setHistoryTotalPages(null);
|
||||||
setHasNextHistoryPage(false);
|
setHasNextHistoryPage(false);
|
||||||
toast({ type: 'error', title: '파이프라인 이력 조회 실패', message: error.message });
|
toast({ type: 'error', title: '파이프라인 이력 조회 실패', message: error.message });
|
||||||
})
|
})
|
||||||
@@ -139,7 +147,10 @@ export function CommonAppBuildPage() {
|
|||||||
<span className={cn('size-2.5 shrink-0 rounded-full', pipelineStatusDotClass(pipeline.status))} />
|
<span className={cn('size-2.5 shrink-0 rounded-full', pipelineStatusDotClass(pipeline.status))} />
|
||||||
<p className="truncate text-sm font-semibold">{formatRepositoryName(pipeline.repository)}</p>
|
<p className="truncate text-sm font-semibold">{formatRepositoryName(pipeline.repository)}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 truncate text-sm text-muted-foreground">{pipeline.message || '-'}</p>
|
<p className="mt-1 truncate text-sm text-muted-foreground">
|
||||||
|
<span className="mr-1 font-mono text-xs">#{pipeline.number ?? '-'}</span>
|
||||||
|
{pipeline.message || '-'}
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -158,31 +169,35 @@ export function CommonAppBuildPage() {
|
|||||||
<div className="flex min-h-40 items-center justify-center px-5 text-sm text-muted-foreground">표시할 파이프라인 이력이 없습니다.</div>
|
<div className="flex min-h-40 items-center justify-center px-5 text-sm text-muted-foreground">표시할 파이프라인 이력이 없습니다.</div>
|
||||||
) : null}
|
) : null}
|
||||||
{history.map((pipeline) => (
|
{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 key={pipeline.id} className="px-5 py-3 transition-colors hover:bg-muted/20">
|
||||||
<div className="min-w-0">
|
<div className="flex min-w-0 items-center justify-between gap-4">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<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={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>
|
<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>
|
<p className="truncate text-sm font-semibold">{pipeline.message || '-'}</p>
|
||||||
</div>
|
</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)}>
|
<Badge tone={pipelineStatusTone(pipeline.status)}>
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
{isActivePipelineStatus(pipeline.status) ? <Loader2 className="size-3 animate-spin" /> : null}
|
{isActivePipelineStatus(pipeline.status) ? <Loader2 className="size-3 animate-spin" /> : null}
|
||||||
{pipeline.status || '-'}
|
{pipeline.status || '-'}
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-2 pl-5 text-xs text-muted-foreground">
|
||||||
<span className="rounded-md bg-muted/50 px-2 py-1">{pipeline.branch || '-'}</span>
|
<span className="rounded-md bg-muted/50 px-2 py-1">{pipeline.branch || '-'}</span>
|
||||||
<span className="rounded-md bg-muted/50 px-2 py-1 font-mono">SHA1 {formatShortSha(pipeline.commitSha)}</span>
|
<span className="rounded-md bg-muted/50 px-2 py-1 font-mono">SHA1 {formatShortSha(pipeline.commitSha)}</span>
|
||||||
<span className="rounded-md bg-muted/50 px-2 py-1">소요 {formatDuration(pipeline.durationSeconds)}</span>
|
<span className="rounded-md bg-muted/50 px-2 py-1">소요 {formatDuration(pipeline.durationSeconds)}</span>
|
||||||
<span className="min-w-16 text-right">{formatPipelineRelativeTime(pipeline.createdAt)}</span>
|
<span className="rounded-md bg-muted/50 px-2 py-1">{formatPipelineRelativeTime(pipeline.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border-t border-border px-5 py-3 text-xs text-muted-foreground">
|
<div className="flex items-center justify-between border-t border-border px-5 py-3 text-xs text-muted-foreground">
|
||||||
<span>현재 {historyPage}페이지</span>
|
<span>
|
||||||
|
{historyTotal !== null && historyTotalPages !== null
|
||||||
|
? `총 ${historyTotal}건 · ${historyPage} / ${historyTotalPages}`
|
||||||
|
: `현재 ${historyPage}페이지`}
|
||||||
|
</span>
|
||||||
<div className="flex gap-2">
|
<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 variant="outline" className="h-8 px-3 text-[13px]" disabled={historyLoading || historyPage <= 1} onClick={() => setHistoryPage((page) => Math.max(1, page - 1))}>
|
||||||
이전
|
이전
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export function listCommonAppPipelineHistory(repoId: string, page = 1, limit = 5
|
|||||||
return apiRequest<{
|
return apiRequest<{
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
total: number | null;
|
||||||
|
totalPages: number | null;
|
||||||
hasNext: boolean;
|
hasNext: boolean;
|
||||||
pipelines: CommonAppPipeline[];
|
pipelines: CommonAppPipeline[];
|
||||||
}>(`/woodpecker/repos/${encodeURIComponent(repoId)}/pipelines?${params.toString()}`);
|
}>(`/woodpecker/repos/${encodeURIComponent(repoId)}/pipelines?${params.toString()}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user