connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { ExternalLink, GitBranch, Loader2, RotateCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Badge, Button, Card, CardHeader } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
@@ -6,12 +6,13 @@ 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';
|
||||
import { formatPipelineRelativeTime, formatRepositoryName, isActivePipelineStatus, pipelineStatusDotClass, pipelineStatusTone } from './common-app-utils';
|
||||
|
||||
export function CommonAppBuildPage() {
|
||||
const { toast } = useToast();
|
||||
const [pipelines, setPipelines] = useState<CommonAppPipeline[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastLoadedAt, setLastLoadedAt] = useState<number | null>(null);
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
|
||||
const runningCount = useMemo(
|
||||
@@ -22,7 +23,10 @@ export function CommonAppBuildPage() {
|
||||
const refresh = (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
listCommonAppPipelines()
|
||||
.then(setPipelines)
|
||||
.then((nextPipelines) => {
|
||||
setPipelines(nextPipelines);
|
||||
setLastLoadedAt(Date.now());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (!silent) toast({ type: 'error', title: '공통앱 빌드 조회 실패', message: error.message });
|
||||
})
|
||||
@@ -36,16 +40,16 @@ export function CommonAppBuildPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => refresh(true), runningCount > 0 ? 3000 : 10000);
|
||||
const timer = window.setInterval(() => refresh(true), 5000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [runningCount]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
eyebrow="공통앱"
|
||||
title="빌드 관리"
|
||||
description="공통앱 저장소의 Woodpecker 파이프라인 진행 상태입니다."
|
||||
description="공통앱 저장소의 최근 파이프라인 상태입니다."
|
||||
actions={
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={() => refresh()} disabled={loading}>
|
||||
<RotateCcw className={cn('size-4', loading && 'animate-spin')} />
|
||||
@@ -54,53 +58,57 @@ export function CommonAppBuildPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<Card className="min-w-0 overflow-hidden">
|
||||
<CardHeader
|
||||
title="모든 저장소"
|
||||
description="마지막 파이프라인 생성 순으로 정렬된 저장소"
|
||||
action={
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="hidden text-xs text-muted-foreground sm:inline">
|
||||
{runningCount > 0 ? `진행 중 ${runningCount}개` : lastLoadedAt ? `${formatPipelineRelativeTime(lastLoadedAt)} 갱신` : '5초마다 갱신'}
|
||||
</span>
|
||||
<Badge tone={runningCount > 0 ? 'blue' : 'neutral'}>{pipelines.length}개</Badge>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="max-h-[680px] overflow-y-auto">
|
||||
{pipelines.length === 0 && !loading ? (
|
||||
<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="px-5 py-4 transition-colors hover:bg-muted/20">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<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))} />
|
||||
<GitBranch className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<p className="truncate text-sm font-semibold">{formatRepositoryName(pipeline.repository)}</p>
|
||||
{isActivePipelineStatus(pipeline.status) ? (
|
||||
<Loader2 className="size-3.5 shrink-0 animate-spin text-sky-300" />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 truncate text-sm text-muted-foreground">{pipeline.message || '-'}</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<Badge tone={pipelineStatusTone(pipeline.status)}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{isActivePipelineStatus(pipeline.status) ? <Loader2 className="size-3 animate-spin" /> : null}
|
||||
{pipeline.status || '-'}
|
||||
</span>
|
||||
</Badge>
|
||||
<span>{pipeline.branch || '-'}</span>
|
||||
<span>{formatPipelineRelativeTime(pipeline.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{pipeline.url ? (
|
||||
<a href={pipeline.url} target="_blank" rel="noreferrer" className="shrink-0 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground" aria-label="저장소 열기">
|
||||
<ExternalLink className="size-4" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user