mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-07-01 07:21:03 +02:00
258 lines
8.8 KiB
TypeScript
258 lines
8.8 KiB
TypeScript
import { useMutation, useQuery } from 'convex/react'
|
|
import { pick } from 'convex-helpers'
|
|
import { ArrowLeft, CheckCircle, Loader2, Share2, XCircle } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { Link, useParams } from 'react-router-dom'
|
|
import { toast } from 'sonner'
|
|
import { Button } from '@/components/ui/button'
|
|
import { humanizeStatus } from '@/lib/utils'
|
|
import { api } from '../../convex/_generated/api'
|
|
import { type BuildFields, buildFields } from '../../convex/schema'
|
|
import { TARGETS } from '../constants/targets'
|
|
|
|
export default function BuildProgress() {
|
|
const { buildHash } = useParams<{ buildHash: string }>()
|
|
const build = useQuery(
|
|
api.builds.getByHash,
|
|
buildHash ? { buildHash } : 'skip'
|
|
)
|
|
const generateDownloadUrl = useMutation(
|
|
api.builds.generateAnonymousDownloadUrl
|
|
)
|
|
const generateSourceDownloadUrl = useMutation(
|
|
api.builds.generateAnonymousSourceDownloadUrl
|
|
)
|
|
const [downloadError, setDownloadError] = useState<string | null>(null)
|
|
const [sourceDownloadError, setSourceDownloadError] = useState<string | null>(
|
|
null
|
|
)
|
|
const [shareUrlCopied, setShareUrlCopied] = useState(false)
|
|
|
|
if (!buildHash) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-white p-8">
|
|
<div className="max-w-4xl mx-auto">
|
|
<p className="text-slate-300">
|
|
Build hash missing.{' '}
|
|
<Link to="/builds/new" className="text-cyan-400">
|
|
Start a new build
|
|
</Link>
|
|
.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (build === undefined) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-white p-8 flex items-center justify-center">
|
|
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!build) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-white p-8">
|
|
<div className="max-w-4xl mx-auto space-y-4">
|
|
<Link
|
|
to="/builds/new"
|
|
className="inline-flex items-center text-slate-400 hover:text-white"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Back to Quick Build
|
|
</Link>
|
|
<div className="bg-slate-900/60 border border-slate-800 rounded-lg p-6">
|
|
<p className="text-slate-300">
|
|
No build found for hash{' '}
|
|
<span className="font-mono">{buildHash}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const targetMeta = build.config.target
|
|
? TARGETS[build.config.target]
|
|
: undefined
|
|
const targetLabel = targetMeta?.name ?? build.config.target
|
|
const status = build.status || 'queued'
|
|
|
|
const handleDownload = async () => {
|
|
setDownloadError(null)
|
|
try {
|
|
const url = await generateDownloadUrl({
|
|
build: pick(build, Object.keys(buildFields) as (keyof BuildFields)[]),
|
|
slug: `quick-build`,
|
|
})
|
|
window.location.href = url
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
setDownloadError('Failed to generate download link.')
|
|
console.error('Download error', message)
|
|
}
|
|
}
|
|
|
|
const handleSourceDownload = async () => {
|
|
setSourceDownloadError(null)
|
|
try {
|
|
const url = await generateSourceDownloadUrl({
|
|
build: pick(build, Object.keys(buildFields) as (keyof BuildFields)[]),
|
|
slug: `quick-build`,
|
|
})
|
|
window.location.href = url
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
setSourceDownloadError('Failed to generate source download link.')
|
|
console.error('Source download error', message)
|
|
}
|
|
}
|
|
|
|
const getStatusIcon = () => {
|
|
if (status === 'success') {
|
|
return <CheckCircle className="w-6 h-6 text-green-500" />
|
|
}
|
|
if (status === 'failure') {
|
|
return <XCircle className="w-6 h-6 text-red-500" />
|
|
}
|
|
return <Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
|
|
}
|
|
|
|
const getStatusColor = () => {
|
|
if (status === 'success') return 'text-green-400'
|
|
if (status === 'failure') return 'text-red-400'
|
|
return 'text-blue-400'
|
|
}
|
|
|
|
const githubActionUrl =
|
|
build.githubRunId && build.githubRunId > 0
|
|
? `https://github.com/MeshEnvy/configurable-web-flasher/actions/runs/${build.githubRunId}`
|
|
: null
|
|
|
|
const shareUrl = `${window.location.origin}/builds/new/${build.buildHash}`
|
|
|
|
const handleShare = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(shareUrl)
|
|
setShareUrlCopied(true)
|
|
toast.success('Share link copied to clipboard')
|
|
setTimeout(() => setShareUrlCopied(false), 2000)
|
|
} catch {
|
|
toast.error('Failed to copy link', {
|
|
description: 'Please copy the URL manually',
|
|
})
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-white p-8">
|
|
<div className="max-w-4xl mx-auto space-y-6">
|
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
<Link
|
|
to="/builds/new"
|
|
className="inline-flex items-center text-slate-400 hover:text-white"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Back to Quick Build
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="bg-slate-900/60 rounded-lg border border-slate-800 p-6 space-y-4">
|
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
<div className="flex items-center gap-4">
|
|
{getStatusIcon()}
|
|
<div>
|
|
<p className="text-sm uppercase tracking-wide text-slate-500">
|
|
Target
|
|
</p>
|
|
<h2 className="text-2xl font-semibold">{targetLabel}</h2>
|
|
<div className="flex items-center gap-2 text-slate-400 mt-1 text-sm">
|
|
<span className={getStatusColor()}>
|
|
{humanizeStatus(status)}
|
|
</span>
|
|
<span>•</span>
|
|
<span>{new Date(build.updatedAt).toLocaleString()}</span>
|
|
{githubActionUrl && (
|
|
<>
|
|
<span>•</span>
|
|
<a
|
|
href={githubActionUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-slate-500 hover:text-slate-300"
|
|
>
|
|
View run
|
|
</a>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
onClick={handleShare}
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
<Share2 className="w-4 h-4 mr-2" />
|
|
{shareUrlCopied ? 'Copied!' : 'Share Build'}
|
|
</Button>
|
|
</div>
|
|
|
|
{status !== 'success' && status !== 'failure' && (
|
|
<div className="rounded-lg border border-slate-800/70 bg-slate-950/60 p-4">
|
|
<p className="text-sm text-slate-400">
|
|
Builds run in GitHub Actions. When the status is
|
|
<span className="text-green-400 font-medium"> success</span>,
|
|
your firmware artifact will be ready to download.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{status === 'success' && build.artifactPath && (
|
|
<div className="space-y-2">
|
|
<Button
|
|
onClick={handleDownload}
|
|
className="w-full bg-cyan-600 hover:bg-cyan-700"
|
|
>
|
|
Download firmware
|
|
</Button>
|
|
{downloadError && (
|
|
<p className="text-sm text-red-400">{downloadError}</p>
|
|
)}
|
|
<Button
|
|
onClick={handleSourceDownload}
|
|
className="w-full bg-slate-700 hover:bg-slate-600"
|
|
variant="outline"
|
|
>
|
|
Download source
|
|
</Button>
|
|
{sourceDownloadError && (
|
|
<p className="text-sm text-red-400">{sourceDownloadError}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{status === 'failure' && (
|
|
<div className="rounded-lg border border-red-500/40 bg-red-500/10 p-4 text-sm text-red-100">
|
|
<p className="font-medium text-red-200">
|
|
Build failed. Please try tweaking your configuration or
|
|
re-running the build.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{status !== 'success' && status !== 'failure' && (
|
|
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-4 text-sm text-blue-100">
|
|
<p className="font-medium text-blue-200">
|
|
This build is still running. Leave this tab open or come back
|
|
later using the URL above.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|