connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-08 17:51:16 +09:00
parent 90a22c098c
commit 38fe8bf098
17 changed files with 655 additions and 649 deletions
@@ -0,0 +1,110 @@
import { RotateCcw } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { Badge, Button, Card, CardHeader } from '../../components/ui';
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 { formatPipelineTime, isActivePipelineStatus, pipelineStatusTone } from './common-app-utils';
export function CommonAppBuildPage() {
const { toast } = useToast();
const [pipelines, setPipelines] = useState<CommonAppPipeline[]>([]);
const [loading, setLoading] = useState(true);
const loadingVisible = useMinimumLoading(loading);
const runningCount = useMemo(
() => pipelines.filter((pipeline) => isActivePipelineStatus(pipeline.status)).length,
[pipelines]
);
const refresh = (silent = false) => {
if (!silent) setLoading(true);
listCommonAppPipelines()
.then(setPipelines)
.catch((error: Error) => {
if (!silent) toast({ type: 'error', title: '공통앱 빌드 조회 실패', message: error.message });
})
.finally(() => {
if (!silent) setLoading(false);
});
};
useEffect(() => {
refresh();
}, []);
useEffect(() => {
const timer = window.setInterval(() => refresh(true), runningCount > 0 ? 3000 : 10000);
return () => window.clearInterval(timer);
}, [runningCount]);
return (
<>
<PageTitle
eyebrow="공통앱"
title="빌드 관리"
description="공통앱 저장소의 Woodpecker 파이프라인 진행 상태입니다."
actions={
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={() => refresh()} disabled={loading}>
<RotateCcw className={cn('size-4', loading && 'animate-spin')} />
새로고침
</Button>
}
/>
<div className="mb-4 grid gap-4 md:grid-cols-3">
<Card className="p-5">
<p className="text-sm text-muted-foreground">진행 중</p>
<p className="mt-3 text-2xl font-semibold">{runningCount}</p>
</Card>
<Card className="p-5">
<p className="text-sm text-muted-foreground">연결 저장소</p>
<p className="mt-3 text-2xl font-semibold">{pipelines.length}</p>
</Card>
<Card className="p-5">
<p className="text-sm text-muted-foreground">갱신 주기</p>
<p className="mt-3 text-2xl font-semibold">{runningCount > 0 ? '3초' : '10초'}</p>
</Card>
</div>
<Card>
<CardHeader title="파이프라인" description="최근 파이프라인 생성 순으로 표시합니다." />
<div className="overflow-x-auto">
<table className="w-full min-w-[920px] whitespace-nowrap text-left text-sm">
<thead className="border-b border-border bg-muted/30 text-xs text-muted-foreground">
<tr>
<th className="px-5 py-3 font-medium">저장소</th>
<th className="px-5 py-3 font-medium">브랜치</th>
<th className="px-5 py-3 font-medium">상태</th>
<th className="px-5 py-3 font-medium">메시지</th>
<th className="px-5 py-3 font-medium">생성시각</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{pipelines.length === 0 && !loading ? (
<tr>
<td colSpan={5} className="px-5 py-10 text-center text-muted-foreground">표시할 파이프라인이 없습니다.</td>
</tr>
) : null}
{pipelines.map((pipeline) => (
<tr key={pipeline.id} className="hover:bg-muted/20">
<td className="px-5 py-4 font-medium">{pipeline.repository}</td>
<td className="px-5 py-4">{pipeline.branch || '-'}</td>
<td className="px-5 py-4">
<Badge tone={pipelineStatusTone(pipeline.status)}>{pipeline.status || '-'}</Badge>
</td>
<td className="max-w-[420px] truncate px-5 py-4">{pipeline.message || '-'}</td>
<td className="px-5 py-4 text-muted-foreground">{formatPipelineTime(pipeline.createdAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
<LoadingModal open={loadingVisible} />
</>
);
}