feat: add BuildDownloadButton component for firmware and source downloads

This commit is contained in:
Ben Allfree
2025-12-01 11:33:18 -08:00
parent 8cba89a9e0
commit 2e22cfe49f
3 changed files with 118 additions and 66 deletions
+92
View File
@@ -0,0 +1,92 @@
import { useMutation } from 'convex/react'
import { pick } from 'convex-helpers'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { api } from '../../convex/_generated/api'
import { type BuildFields, buildFields } from '../../convex/schema'
interface BuildDownloadButtonProps {
build: BuildFields
type: 'firmware' | 'source'
variant?: 'default' | 'outline'
className?: string
}
export function BuildDownloadButton({
build,
type,
variant,
className,
}: BuildDownloadButtonProps) {
const generateDownloadUrl = useMutation(
api.builds.generateAnonymousDownloadUrl
)
const generateSourceDownloadUrl = useMutation(
api.builds.generateAnonymousSourceDownloadUrl
)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Default styling based on type
const defaultVariant =
variant ?? (type === 'firmware' ? 'default' : 'outline')
const defaultClassName =
className ??
(type === 'firmware'
? 'bg-cyan-600 hover:bg-cyan-700'
: 'bg-slate-700 hover:bg-slate-600')
const handleDownload = async () => {
setError(null)
setIsLoading(true)
try {
const url =
type === 'firmware'
? await generateDownloadUrl({
build: pick(
build,
Object.keys(buildFields) as (keyof BuildFields)[]
),
slug: 'download',
})
: await generateSourceDownloadUrl({
build: pick(
build,
Object.keys(buildFields) as (keyof BuildFields)[]
),
slug: 'download',
})
window.location.href = url
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const errorMsg =
type === 'firmware'
? 'Failed to generate download link.'
: 'Failed to generate source download link.'
setError(errorMsg)
toast.error(errorMsg, {
description: message,
})
} finally {
setIsLoading(false)
}
}
if (type === 'firmware' && !build.artifactPath) return null
if (type === 'source' && !build.sourceUrl && !build.buildHash) return null
return (
<div className="space-y-2">
<Button
onClick={handleDownload}
disabled={isLoading}
variant={defaultVariant}
className={defaultClassName}
>
Download {type === 'firmware' ? 'firmware' : 'source'}
</Button>
{error && <p className="text-sm text-red-400">{error}</p>}
</div>
)
}
+13
View File
@@ -2,6 +2,7 @@ import { useMutation, useQuery } from 'convex/react'
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { BuildDownloadButton } from '@/components/BuildDownloadButton'
import { Button } from '@/components/ui/button'
import { api } from '../../convex/_generated/api'
import type { Id } from '../../convex/_generated/dataModel'
@@ -201,6 +202,18 @@ export default function Admin() {
))}
</div>
)}
{(build.artifactPath ||
build.sourceUrl ||
build.buildHash) && (
<div className="mt-3 flex gap-3">
{build.artifactPath && (
<BuildDownloadButton build={build} type="firmware" />
)}
{(build.sourceUrl || build.buildHash) && (
<BuildDownloadButton build={build} type="source" />
)}
</div>
)}
</div>
<div className="ml-4">
<Button
+13 -66
View File
@@ -1,5 +1,4 @@
import { useMutation, useQuery } from 'convex/react'
import { pick } from 'convex-helpers'
import { useQuery } from 'convex/react'
import {
AlertCircle,
ArrowLeft,
@@ -11,10 +10,10 @@ import {
import { useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { toast } from 'sonner'
import { BuildDownloadButton } from '@/components/BuildDownloadButton'
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() {
@@ -23,16 +22,6 @@ export default function BuildProgress() {
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) {
@@ -87,36 +76,6 @@ export default function BuildProgress() {
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" />
@@ -274,32 +233,20 @@ export default function BuildProgress() {
)}
{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>
)}
</div>
<BuildDownloadButton
build={build}
type="firmware"
className="w-full bg-cyan-600 hover:bg-cyan-700"
/>
)}
{build.sourceUrl && (
<div className="space-y-2">
<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>
<BuildDownloadButton
build={build}
type="source"
variant="outline"
className="w-full bg-slate-700 hover:bg-slate-600"
/>
)}
{status === 'failure' && (