Skip to content
Clone
f0a1552
70 lines 3.9 KB
import { useState } from 'react'
import { CircleCheck, CircleDashed, CircleX, Clock, Loader } from 'lucide-react'
import { ErrorPanel, PanelNote } from '@/shared/ui'
import { fmtRelative, useAsync } from '@/shared/lib'
import { getPullChecks, type PullRef } from '@/entities/pull-request'
import { Copyable } from './parts'
const RUN_STATE: Record<string, { label: string; icon: typeof Clock; tone: string }> = {
pending: { label: '排队中', icon: CircleDashed, tone: 'text-shell-ink-muted' },
running: { label: '运行中', icon: Loader, tone: 'text-[#8a6100]' },
success: { label: '通过', icon: CircleCheck, tone: 'text-[#1a7f37]' },
failed: { label: '失败', icon: CircleX, tone: 'text-[#b42318]' },
cancelled: { label: '已取消', icon: Clock, tone: 'text-shell-ink-muted' },
}
const STATUS_STATE: Record<string, { label: string; tone: string }> = {
pending: { label: '等待', tone: 'text-shell-ink-muted' },
success: { label: '通过', tone: 'text-[#1a7f37]' },
failure: { label: '失败', tone: 'text-[#b42318]' },
error: { label: '错误', tone: 'text-[#b42318]' },
}
/** 检查页签:最新 head 的流水线运行 + 提交状态。 */
export function ChecksTab({ refObj, headSha }: { refObj: PullRef; headSha: string }) {
const [tick, setTick] = useState(0)
const checks = useAsync(() => getPullChecks(refObj), [refObj.space, refObj.repo, refObj.pull, tick])
const failure = checks.error as { code?: number; message?: string } | undefined
if (checks.loading && !checks.data) return <PanelNote text="加载检查…" />
if (failure) return <ErrorPanel status={failure.code ?? 0} what="检查" note={failure.message} onRetry={() => setTick(t => t + 1)} />
const data = checks.data
if (!data) return <PanelNote text="没有检查数据。" />
const runs = data.runs ?? []
const statuses = data.statuses ?? []
if (!runs.length && !statuses.length) {
return <PanelNote text={`该版本(${(data.head_sha || headSha).slice(0, 8)})没有流水线运行或提交状态。`} />
}
return (
<div className="space-y-3">
<p className="text-[12.5px] text-shell-ink-muted">版本 <Copyable text={data.head_sha || headSha} /></p>
{runs.map(run => {
const hit = RUN_STATE[run.state] ?? { label: run.state, icon: Clock, tone: 'text-shell-ink-muted' }
const Icon = hit.icon
return (
<div key={run.id} className="flex flex-wrap items-center gap-2 rounded-xl border border-shell-hairline bg-shell-surface px-4 py-2.5 text-[13px]">
<Icon className={`h-4 w-4 shrink-0 ${hit.tone}`} aria-hidden />
<span className="font-medium">流水线 #{run.id.slice(0, 8)}</span>
<span className={`text-[12px] ${hit.tone}`}>{hit.label}</span>
<span className="text-[12px] text-shell-ink-muted">{run.branch || '—'} · {fmtRelative(run.created_at)}</span>
</div>
)
})}
{statuses.map((status, index) => {
const hit = STATUS_STATE[status.state] ?? { label: status.state, tone: 'text-shell-ink-muted' }
return (
<div key={`${status.context}:${index}`} className="flex flex-wrap items-center gap-2 rounded-xl border border-shell-hairline bg-shell-surface px-4 py-2.5 text-[13px]">
<span className={`h-2 w-2 shrink-0 rounded-full bg-current ${hit.tone}`} aria-hidden />
<span className="font-medium">{status.context}</span>
<span className={`text-[12px] ${hit.tone}`}>{hit.label}</span>
{status.description ? <span className="text-[12px] text-shell-ink-muted">{status.description}</span> : null}
{status.target_url ? (
<button type="button" onClick={() => status.target_url && window.open(status.target_url, '_blank', 'noopener,noreferrer')} className="ml-auto text-[12px] text-shell-ink-muted underline underline-offset-2 hover:text-shell-ink">
详情
</button>
) : null}
</div>
)
})}
</div>
)
}