mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-26 22:54:54 +00:00
Generated by Spark: Display build status badges for specific workflows or branches
This commit is contained in:
@@ -2,15 +2,19 @@ import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
GitBranch,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
ArrowSquareOut,
|
||||
Warning
|
||||
Warning,
|
||||
Copy,
|
||||
CheckSquare
|
||||
} from '@phosphor-icons/react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface WorkflowRun {
|
||||
id: number
|
||||
@@ -23,42 +27,58 @@ interface WorkflowRun {
|
||||
head_branch: string
|
||||
head_sha: string
|
||||
event: string
|
||||
workflow_id: number
|
||||
path: string
|
||||
}
|
||||
|
||||
interface Workflow {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
state: string
|
||||
badge_url: string
|
||||
}
|
||||
|
||||
interface GitHubBuildStatusProps {
|
||||
owner: string
|
||||
repo: string
|
||||
defaultBranch?: string
|
||||
}
|
||||
|
||||
export function GitHubBuildStatus({ owner, repo }: GitHubBuildStatusProps) {
|
||||
export function GitHubBuildStatus({ owner, repo, defaultBranch = 'main' }: GitHubBuildStatusProps) {
|
||||
const [workflows, setWorkflows] = useState<WorkflowRun[]>([])
|
||||
const [allWorkflows, setAllWorkflows] = useState<Workflow[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copiedBadge, setCopiedBadge] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkflowRuns()
|
||||
fetchData()
|
||||
}, [owner, repo])
|
||||
|
||||
const fetchWorkflowRuns = async () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/actions/runs?per_page=5`,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
},
|
||||
}
|
||||
)
|
||||
const [runsResponse, workflowsResponse] = await Promise.all([
|
||||
fetch(`https://api.github.com/repos/${owner}/${repo}/actions/runs?per_page=5`, {
|
||||
headers: { 'Accept': 'application/vnd.github.v3+json' },
|
||||
}),
|
||||
fetch(`https://api.github.com/repos/${owner}/${repo}/actions/workflows`, {
|
||||
headers: { 'Accept': 'application/vnd.github.v3+json' },
|
||||
})
|
||||
])
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status}`)
|
||||
if (!runsResponse.ok || !workflowsResponse.ok) {
|
||||
throw new Error(`GitHub API error: ${runsResponse.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setWorkflows(data.workflow_runs || [])
|
||||
const runsData = await runsResponse.json()
|
||||
const workflowsData = await workflowsResponse.json()
|
||||
|
||||
setWorkflows(runsData.workflow_runs || [])
|
||||
setAllWorkflows(workflowsData.workflows || [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch workflows')
|
||||
} finally {
|
||||
@@ -66,6 +86,29 @@ export function GitHubBuildStatus({ owner, repo }: GitHubBuildStatusProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const getBadgeUrl = (workflowPath: string, branch?: string) => {
|
||||
const workflowFile = workflowPath.split('/').pop()
|
||||
if (branch) {
|
||||
return `https://github.com/${owner}/${repo}/actions/workflows/${workflowFile}/badge.svg?branch=${branch}`
|
||||
}
|
||||
return `https://github.com/${owner}/${repo}/actions/workflows/${workflowFile}/badge.svg`
|
||||
}
|
||||
|
||||
const getBadgeMarkdown = (workflowPath: string, workflowName: string, branch?: string) => {
|
||||
const badgeUrl = getBadgeUrl(workflowPath, branch)
|
||||
const actionUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflowPath.split('/').pop()}`
|
||||
return `[](${actionUrl})`
|
||||
}
|
||||
|
||||
const copyBadgeMarkdown = (workflowPath: string, workflowName: string, branch?: string) => {
|
||||
const markdown = getBadgeMarkdown(workflowPath, workflowName, branch)
|
||||
navigator.clipboard.writeText(markdown)
|
||||
const key = `${workflowPath}-${branch || 'default'}`
|
||||
setCopiedBadge(key)
|
||||
toast.success('Badge markdown copied to clipboard')
|
||||
setTimeout(() => setCopiedBadge(null), 2000)
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string, conclusion: string | null) => {
|
||||
if (status === 'completed') {
|
||||
if (conclusion === 'success') {
|
||||
@@ -156,7 +199,7 @@ export function GitHubBuildStatus({ owner, repo }: GitHubBuildStatusProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={fetchWorkflowRuns}
|
||||
onClick={fetchData}
|
||||
className="text-xs"
|
||||
>
|
||||
Try Again
|
||||
@@ -168,7 +211,7 @@ export function GitHubBuildStatus({ owner, repo }: GitHubBuildStatusProps) {
|
||||
)
|
||||
}
|
||||
|
||||
if (workflows.length === 0) {
|
||||
if (workflows.length === 0 && allWorkflows.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -187,6 +230,8 @@ export function GitHubBuildStatus({ owner, repo }: GitHubBuildStatusProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const uniqueBranches = Array.from(new Set(workflows.map(w => w.head_branch)))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -196,75 +241,167 @@ export function GitHubBuildStatus({ owner, repo }: GitHubBuildStatusProps) {
|
||||
<GitBranch size={24} weight="duotone" />
|
||||
GitHub Actions
|
||||
</CardTitle>
|
||||
<CardDescription>Recent workflow runs from {owner}/{repo}</CardDescription>
|
||||
<CardDescription>Build status badges and recent workflow runs</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={fetchWorkflowRuns}
|
||||
onClick={fetchData}
|
||||
className="text-xs"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="flex items-center justify-between p-3 border border-border rounded-lg hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{getStatusIcon(workflow.status, workflow.conclusion)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{workflow.name}
|
||||
</p>
|
||||
{getStatusBadge(workflow.status, workflow.conclusion)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
|
||||
<span className="truncate">{workflow.head_branch}</span>
|
||||
<span>•</span>
|
||||
<span>{formatTime(workflow.updated_at)}</span>
|
||||
<span>•</span>
|
||||
<span className="truncate">{workflow.event}</span>
|
||||
<CardContent className="space-y-6">
|
||||
<Tabs defaultValue="badges" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="badges">Status Badges</TabsTrigger>
|
||||
<TabsTrigger value="runs">Recent Runs</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="badges" className="space-y-4 mt-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">Workflow Badges</h3>
|
||||
<div className="space-y-3">
|
||||
{allWorkflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="p-3 border border-border rounded-lg space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">{workflow.name}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyBadgeMarkdown(workflow.path, workflow.name)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{copiedBadge === `${workflow.path}-default` ? (
|
||||
<CheckSquare size={14} className="text-green-500" />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<img
|
||||
src={getBadgeUrl(workflow.path)}
|
||||
alt={`${workflow.name} status`}
|
||||
className="h-5"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground font-mono break-all">
|
||||
{getBadgeMarkdown(workflow.path, workflow.name)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{uniqueBranches.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">Branch-Specific Badges</h3>
|
||||
<div className="space-y-3">
|
||||
{uniqueBranches.slice(0, 3).map((branch) => (
|
||||
<div key={branch} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch size={16} weight="duotone" />
|
||||
<p className="text-sm font-medium">{branch}</p>
|
||||
</div>
|
||||
{allWorkflows.slice(0, 2).map((workflow) => (
|
||||
<div
|
||||
key={`${workflow.id}-${branch}`}
|
||||
className="p-3 border border-border rounded-lg space-y-2 ml-6"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">{workflow.name}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyBadgeMarkdown(workflow.path, workflow.name, branch)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{copiedBadge === `${workflow.path}-${branch}` ? (
|
||||
<CheckSquare size={14} className="text-green-500" />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<img
|
||||
src={getBadgeUrl(workflow.path, branch)}
|
||||
alt={`${workflow.name} status on ${branch}`}
|
||||
className="h-5"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="runs" className="space-y-3 mt-4">
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="flex items-center justify-between p-3 border border-border rounded-lg hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{getStatusIcon(workflow.status, workflow.conclusion)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{workflow.name}
|
||||
</p>
|
||||
{getStatusBadge(workflow.status, workflow.conclusion)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
|
||||
<span className="truncate">{workflow.head_branch}</span>
|
||||
<span>•</span>
|
||||
<span>{formatTime(workflow.updated_at)}</span>
|
||||
<span>•</span>
|
||||
<span className="truncate">{workflow.event}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="ml-2"
|
||||
>
|
||||
<a
|
||||
href={workflow.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<ArrowSquareOut size={16} />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
asChild
|
||||
className="ml-2"
|
||||
className="w-full text-xs"
|
||||
>
|
||||
<a
|
||||
href={workflow.html_url}
|
||||
href={`https://github.com/${owner}/${repo}/actions`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowSquareOut size={16} />
|
||||
View All Workflows
|
||||
<ArrowSquareOut size={14} />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
asChild
|
||||
className="w-full text-xs"
|
||||
>
|
||||
<a
|
||||
href={`https://github.com/${owner}/${repo}/actions`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
View All Workflows
|
||||
<ArrowSquareOut size={14} />
|
||||
</a>
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user