104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
'use client'
|
|||
|
|
|
||
|
|
import { useState, useEffect } from 'react'
|
||
|
|
import { motion } from 'framer-motion'
|
||
|
|
import { GitBranch, CheckCircle, XCircle, Loader2, AlertCircle } from 'lucide-react'
|
||
|
|
|
||
|
|
interface CIStatus {
|
||
|
|
status: string
|
||
|
|
conclusion?: string
|
||
|
|
title?: string
|
||
|
|
branch?: string
|
||
|
|
url?: string
|
||
|
|
updatedAt?: string
|
||
|
|
error?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function CIStatusIndicator() {
|
||
|
|
const [ciStatus, setCIStatus] = useState<CIStatus | null>(null)
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const fetchStatus = async () => {
|
||
|
|
try {
|
||
|
|
const res = await fetch('/api/ci-status')
|
||
|
|
const data = await res.json()
|
||
|
|
setCIStatus(data)
|
||
|
|
} catch {
|
||
|
|
setCIStatus({ status: 'error', error: 'Failed to fetch' })
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fetchStatus()
|
||
|
|
// Poll every 60 seconds
|
||
|
|
const interval = setInterval(fetchStatus, 60000)
|
||
|
|
return () => clearInterval(interval)
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const getStatusIcon = () => {
|
||
|
|
if (loading) return <Loader2 size={14} className="animate-spin" />
|
||
|
|
if (!ciStatus || ciStatus.status === 'error') return <AlertCircle size={14} />
|
||
|
|
if (ciStatus.conclusion === 'success') return <CheckCircle size={14} />
|
||
|
|
if (ciStatus.conclusion === 'failure') return <XCircle size={14} />
|
||
|
|
if (ciStatus.status === 'in_progress' || ciStatus.status === 'queued') {
|
||
|
|
return <Loader2 size={14} className="animate-spin" />
|
||
|
|
}
|
||
|
|
return <GitBranch size={14} />
|
||
|
|
}
|
||
|
|
|
||
|
|
const getStatusColor = () => {
|
||
|
|
if (loading) return 'text-gray-400'
|
||
|
|
if (!ciStatus || ciStatus.status === 'error') return 'text-gray-400'
|
||
|
|
if (ciStatus.conclusion === 'success') return 'text-green-500'
|
||
|
|
if (ciStatus.conclusion === 'failure') return 'text-red-500'
|
||
|
|
if (ciStatus.status === 'in_progress' || ciStatus.status === 'queued') {
|
||
|
|
return 'text-yellow-500'
|
||
|
|
}
|
||
|
|
return 'text-gray-400'
|
||
|
|
}
|
||
|
|
|
||
|
|
const getStatusText = () => {
|
||
|
|
if (loading) return 'Checking...'
|
||
|
|
if (!ciStatus || ciStatus.status === 'error') return 'Unknown'
|
||
|
|
if (ciStatus.conclusion === 'success') return 'Passing'
|
||
|
|
if (ciStatus.conclusion === 'failure') return 'Failed'
|
||
|
|
if (ciStatus.status === 'in_progress') return 'Running'
|
||
|
|
if (ciStatus.status === 'queued') return 'Queued'
|
||
|
|
return ciStatus.status
|
||
|
|
}
|
||
|
|
|
||
|
|
const content = (
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, x: 20 }}
|
||
|
|
animate={{ opacity: 1, x: 0 }}
|
||
|
|
transition={{ duration: 0.5, delay: 0.8 }}
|
||
|
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm border border-gray-200 dark:border-gray-700 shadow-sm ${getStatusColor()}`}
|
||
|
|
>
|
||
|
|
<GitBranch size={14} className="text-gray-500 dark:text-gray-400" />
|
||
|
|
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">CI</span>
|
||
|
|
<div className={`flex items-center gap-1 ${getStatusColor()}`}>
|
||
|
|
{getStatusIcon()}
|
||
|
|
<span className="text-xs font-semibold">{getStatusText()}</span>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
)
|
||
|
|
|
||
|
|
if (ciStatus?.url) {
|
||
|
|
return (
|
||
|
|
<a
|
||
|
|
href={ciStatus.url}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
className="hover:scale-105 transition-transform"
|
||
|
|
title={ciStatus.title || 'View CI run'}
|
||
|
|
>
|
||
|
|
{content}
|
||
|
|
</a>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return content
|
||
|
|
}
|