mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-07-05 17:32:11 +02:00
feat: add BuildProgress component and enhance BuildDownloadButton with compact mode
This commit is contained in:
@@ -12,9 +12,10 @@ interface BuildDownloadButtonProps {
|
||||
type: ArtifactType
|
||||
variant?: "default" | "outline"
|
||||
className?: string
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export function BuildDownloadButton({ build, type, variant, className }: BuildDownloadButtonProps) {
|
||||
export function BuildDownloadButton({ build, type, variant, className, compact = false }: BuildDownloadButtonProps) {
|
||||
const generateDownloadUrl = useMutation(api.builds.generateDownloadUrl)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -50,11 +51,54 @@ export function BuildDownloadButton({ build, type, variant, className }: BuildDo
|
||||
|
||||
if (type === ArtifactType.Firmware && !build.buildHash) return null
|
||||
|
||||
const buttonElement = (
|
||||
<Button onClick={handleDownload} disabled={isLoading} variant={defaultVariant} className={defaultClassName}>
|
||||
{type === ArtifactType.Firmware && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<rect width="3" height="4.5" x="3.25" y="1.75" />
|
||||
<path d="m9.75 6.25h3m-3-4.5h1.5v4" />
|
||||
<rect width="3" height="4.5" x="9.75" y="9.75" />
|
||||
<path d="m3.25 14.25h3m-3-4.5h1.5v4" />
|
||||
</svg>
|
||||
)}
|
||||
{type === ArtifactType.Source && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M9.5 5H9a2 2 0 0 0-2 2v2c0 1-.6 3-3 3c1 0 3 .6 3 3v2a2 2 0 0 0 2 2h.5m5-14h.5a2 2 0 0 1 2 2v2c0 1 .6 3 3 3c-1 0-3 .6-3 3v2a2 2 0 0 1-2 2h-.5" />
|
||||
</svg>
|
||||
)}
|
||||
Download {type === ArtifactType.Firmware ? "firmware" : "source"}
|
||||
</Button>
|
||||
)
|
||||
|
||||
if (compact) {
|
||||
return buttonElement
|
||||
}
|
||||
|
||||
const button = (
|
||||
<div className="space-y-2">
|
||||
<Button onClick={handleDownload} disabled={isLoading} variant={defaultVariant} className={defaultClassName}>
|
||||
Download {type === ArtifactType.Firmware ? "firmware" : "source"}
|
||||
</Button>
|
||||
{buttonElement}
|
||||
{type === ArtifactType.Firmware && (
|
||||
<p className="text-xs text-slate-400 text-center">
|
||||
Need help flashing?{" "}
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
import { BuildDownloadButton } from "@/components/BuildDownloadButton"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { TARGETS } from "@/constants/targets"
|
||||
import type { Doc } from "@/convex/_generated/dataModel"
|
||||
import { ArtifactType } from "@/convex/builds"
|
||||
import { getArtifactFilenameBase } from "@/convex/lib/filename"
|
||||
import modulesData from "@/convex/modules.json"
|
||||
import { getImplicitDependencies, humanizeStatus } from "@/lib/utils"
|
||||
import registryData from "@/public/registry.json"
|
||||
import { AlertCircle, CheckCircle, Copy, Loader2, Share2, X, XCircle } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { navigate } from "vike/client/router"
|
||||
|
||||
interface BuildProgressProps {
|
||||
build: Doc<"builds">
|
||||
isAdmin?: boolean
|
||||
onRetry?: (buildId: Doc<"builds">["_id"]) => Promise<void>
|
||||
showActions?: boolean
|
||||
}
|
||||
|
||||
export function BuildProgress({ build, isAdmin = false, onRetry, showActions = true }: BuildProgressProps) {
|
||||
const [shareUrlCopied, setShareUrlCopied] = useState(false)
|
||||
const [bashCopied, setBashCopied] = useState(false)
|
||||
const [showBashModal, setShowBashModal] = useState(false)
|
||||
|
||||
const targetMeta = build.config.target ? TARGETS[build.config.target] : undefined
|
||||
const targetLabel = targetMeta?.name ?? build.config.target
|
||||
const status = build.status || "queued"
|
||||
|
||||
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/mesh-forge/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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const generateBashCommand = (): string => {
|
||||
const flags = computeFlagsFromConfig(build.config)
|
||||
const target = build.config.target
|
||||
const version = build.config.version
|
||||
const plugins = build.config.pluginsEnabled || []
|
||||
const commands = []
|
||||
|
||||
// Generate directory name matching the download filename format (without .tar.gz extension)
|
||||
const dirName = getArtifactFilenameBase(version, target, build.buildHash, build.githubRunId, "source")
|
||||
|
||||
// Clone firmware repository into named directory
|
||||
commands.push(`git clone --recursive https://github.com/meshtastic/firmware.git ${dirName}`)
|
||||
commands.push(`cd ${dirName}`)
|
||||
|
||||
// Checkout the specific version
|
||||
commands.push(`git checkout ${version}`)
|
||||
|
||||
// Update submodules after checkout
|
||||
commands.push(`git submodule update --init --recursive`)
|
||||
|
||||
// Install PlatformIO if not already installed
|
||||
commands.push(`pip install platformio`)
|
||||
|
||||
// Install mesh-plugin-manager
|
||||
commands.push(`pip install mesh-plugin-manager`)
|
||||
|
||||
// Initialize mpm
|
||||
commands.push(`mpm init`)
|
||||
|
||||
// Install plugins if any
|
||||
if (plugins.length > 0) {
|
||||
const pluginSlugs = plugins.map(plugin => {
|
||||
// Extract slug from "slug@version" format if present
|
||||
return plugin.includes("@") ? plugin.split("@")[0] : plugin
|
||||
})
|
||||
commands.push(`mpm install ${pluginSlugs.join(" ")}`)
|
||||
}
|
||||
|
||||
// Set build flags and build
|
||||
if (flags) {
|
||||
commands.push(`export PLATFORMIO_BUILD_FLAGS="${flags}"`)
|
||||
}
|
||||
commands.push(`pio run -e ${target}`)
|
||||
|
||||
return commands.join("\n")
|
||||
}
|
||||
|
||||
const handleOpenBashModal = () => {
|
||||
setBashCopied(false)
|
||||
setShowBashModal(true)
|
||||
}
|
||||
|
||||
const handleCopyBashFromModal = async () => {
|
||||
try {
|
||||
const bashCommand = generateBashCommand()
|
||||
await navigator.clipboard.writeText(bashCommand)
|
||||
setBashCopied(true)
|
||||
toast.success("Bash command copied to clipboard")
|
||||
setTimeout(() => setBashCopied(false), 2000)
|
||||
} catch {
|
||||
toast.error("Failed to copy command", {
|
||||
description: "Please copy the command manually",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compute build flags from config (same logic as computeFlagsFromConfig in convex/builds.ts)
|
||||
const computeFlagsFromConfig = (config: typeof build.config): string => {
|
||||
return Object.keys(config.modulesExcluded)
|
||||
.sort()
|
||||
.filter(module => config.modulesExcluded[module])
|
||||
.map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
// Generate GitHub discussion URL with prefilled body
|
||||
const generateDiscussionUrl = (): string => {
|
||||
const flags = computeFlagsFromConfig(build.config)
|
||||
const plugins = build.config.pluginsEnabled?.join(", ") || "(none)"
|
||||
const timestamp = new Date(build.startedAt).toISOString()
|
||||
const githubRunLink = githubActionUrl ? `[View run](${githubActionUrl})` : "(not available)"
|
||||
const buildPageUrl = `${window.location.origin}/builds/${build.buildHash}`
|
||||
|
||||
// Format plugins as +plugin@version
|
||||
const formattedPlugins =
|
||||
build.config.pluginsEnabled
|
||||
?.map(plugin => {
|
||||
// Plugin might be "slug@version" or just "slug"
|
||||
return plugin.includes("@") ? `+${plugin}` : `+${plugin}`
|
||||
})
|
||||
.join(" ") || ""
|
||||
|
||||
const bracketContent = [
|
||||
build.config.target,
|
||||
`v${build.config.version}`,
|
||||
...(formattedPlugins ? [formattedPlugins] : []),
|
||||
].join(" ")
|
||||
|
||||
const discussionTitle = `Build ${build.status === "failure" ? "Failed" : "Issue"}: ${targetLabel} [${bracketContent}]`
|
||||
|
||||
const discussionBody = `## Build ${build.status === "failure" ? "Failed" : "Information"}
|
||||
|
||||
**Build Hash**: \`${build.buildHash}\`
|
||||
**Target Board**: ${build.config.target}
|
||||
**Firmware Version**: ${build.config.version}
|
||||
**Build Flags**: ${flags || "(none)"}
|
||||
**Plugins**: ${plugins}
|
||||
**Build Timestamp**: ${timestamp}
|
||||
|
||||
**Build Page**: [View build page](${buildPageUrl})
|
||||
**GitHub Run**: ${githubRunLink}
|
||||
|
||||
## Additional Information
|
||||
(Please add any additional details about the issue here)`
|
||||
|
||||
const baseUrl = "https://github.com/MeshEnvy/mesh-forge/discussions/new"
|
||||
const params = new URLSearchParams({
|
||||
category: "q-a",
|
||||
title: discussionTitle,
|
||||
body: discussionBody,
|
||||
})
|
||||
|
||||
return `${baseUrl}?${params.toString()}`
|
||||
}
|
||||
|
||||
const handleReportIssue = () => {
|
||||
window.open(generateDiscussionUrl(), "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
const handleRetry = async () => {
|
||||
if (!build?._id || !onRetry) return
|
||||
try {
|
||||
await onRetry(build._id)
|
||||
} catch (error) {
|
||||
toast.error("Failed to retry build", {
|
||||
description: String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Get excluded modules
|
||||
const excludedModules = modulesData.modules.filter(module => build.config.modulesExcluded[module.id] === true)
|
||||
|
||||
// Get explicitly selected plugins from stored config
|
||||
// The stored config only contains explicitly selected plugins (not resolved dependencies)
|
||||
const explicitPluginSlugs = (build.config.pluginsEnabled || []).map(pluginId => {
|
||||
// Extract slug from "slug@version" format if present
|
||||
return pluginId.includes("@") ? pluginId.split("@")[0] : pluginId
|
||||
})
|
||||
|
||||
// Get implicit dependencies (dependencies that are not explicitly selected)
|
||||
const implicitDeps = getImplicitDependencies(
|
||||
explicitPluginSlugs,
|
||||
registryData as Record<string, { dependencies?: Record<string, string> }>
|
||||
)
|
||||
|
||||
// Separate explicit and implicit plugins
|
||||
const explicitPlugins: Array<{
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
}> = []
|
||||
const implicitPlugins: Array<{
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
}> = []
|
||||
|
||||
// Process explicitly selected plugins
|
||||
;(build.config.pluginsEnabled || []).forEach(pluginId => {
|
||||
// Extract slug from "slug@version" format if present
|
||||
const slug = pluginId.includes("@") ? pluginId.split("@")[0] : pluginId
|
||||
const pluginData = (registryData as Record<string, { name: string; description: string; version: string }>)[slug]
|
||||
const pluginInfo = {
|
||||
id: slug,
|
||||
name: pluginData?.name || slug,
|
||||
description: pluginData?.description || "",
|
||||
version: pluginId.includes("@") ? pluginId.split("@")[1] : pluginData?.version || "",
|
||||
}
|
||||
explicitPlugins.push(pluginInfo)
|
||||
})
|
||||
|
||||
// Process implicit dependencies (resolved but not in stored config)
|
||||
for (const slug of implicitDeps) {
|
||||
const pluginData = (registryData as Record<string, { name: string; description: string; version: string }>)[slug]
|
||||
if (pluginData) {
|
||||
implicitPlugins.push({
|
||||
id: slug,
|
||||
name: pluginData.name || slug,
|
||||
description: pluginData.description || "",
|
||||
version: pluginData.version || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900/60 rounded-lg border border-slate-800 p-6 space-y-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-2 flex items-center gap-2">
|
||||
{getStatusIcon()}
|
||||
{targetLabel}
|
||||
{status !== "success" && status !== "failure" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (githubActionUrl) {
|
||||
window.open(githubActionUrl, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
}}
|
||||
disabled={!githubActionUrl}
|
||||
className={`inline-flex items-center gap-1 px-3 py-0.5 rounded-full text-xs font-semibold ${getStatusColor()} bg-slate-800 border border-slate-700 hover:bg-slate-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
style={{ cursor: githubActionUrl ? "pointer" : "not-allowed" }}
|
||||
title={githubActionUrl ? "View GitHub Actions run" : "GitHub run not available"}
|
||||
>
|
||||
{humanizeStatus(status)}
|
||||
</button>
|
||||
)}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 text-slate-400 text-sm">
|
||||
<span className="font-mono">{build.config.target}</span>
|
||||
<span>•</span>
|
||||
<span>v{build.config.version}</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{build.completedAt
|
||||
? new Date(build.completedAt).toLocaleString()
|
||||
: new Date(build.updatedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showActions && (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleReportIssue} variant="outline" className="border-slate-600 hover:bg-slate-800">
|
||||
<AlertCircle className="w-4 h-4 mr-2" />
|
||||
Support
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(`/builds/new/${build.buildHash}`)}
|
||||
variant="outline"
|
||||
className="border-slate-600 hover:bg-slate-800"
|
||||
aria-label="Clone"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 512 512"
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M472 16H160a24.027 24.027 0 0 0-24 24v312a24.027 24.027 0 0 0 24 24h312a24.027 24.027 0 0 0 24-24V40a24.027 24.027 0 0 0-24-24m-8 328H168V48h296Z" />
|
||||
<path d="M344 464H48V168h56v-32H40a24.027 24.027 0 0 0-24 24v312a24.027 24.027 0 0 0 24 24h312a24.027 24.027 0 0 0 24-24v-64h-32Z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
variant="outline"
|
||||
className="border-slate-600 hover:bg-slate-800"
|
||||
aria-label={shareUrlCopied ? "Copied!" : "Share Build"}
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{excludedModules.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm mt-2">
|
||||
<span className="text-slate-500">Excluded Modules:</span>
|
||||
{excludedModules.map((module, index) => (
|
||||
<span key={module.id}>
|
||||
<span className="text-slate-300">{module.name}</span>
|
||||
{index < excludedModules.length - 1 && <span className="text-slate-500">,</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(explicitPlugins.length > 0 || implicitPlugins.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm mt-2">
|
||||
<span className="text-slate-500">Plugins:</span>
|
||||
{explicitPlugins.map((plugin, index) => (
|
||||
<span key={plugin.id}>
|
||||
<a href={`/plugins/${plugin.id}`} className="text-cyan-400 hover:text-cyan-300 underline">
|
||||
{plugin.name}
|
||||
</a>
|
||||
{(index < explicitPlugins.length - 1 || implicitPlugins.length > 0) && (
|
||||
<span className="text-slate-500">,</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{implicitPlugins.map((plugin, index) => (
|
||||
<span key={plugin.id}>
|
||||
<a href={`/plugins/${plugin.id}`} className="text-cyan-400 hover:text-cyan-300 underline">
|
||||
{plugin.name}
|
||||
</a>
|
||||
{index < implicitPlugins.length - 1 && <span className="text-slate-500">,</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{build && (build.githubRunId || (build.githubRunIdHistory?.length ?? 0) > 0) && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm">
|
||||
<span className="text-slate-500">
|
||||
Run History
|
||||
{(build.githubRunIdHistory?.length ?? 0) > 0 &&
|
||||
` (${(build.githubRunIdHistory?.length ?? 0) + (build.githubRunId ? 1 : 0)} total)`}
|
||||
</span>
|
||||
{build.githubRunId && (
|
||||
<a
|
||||
href={`https://github.com/MeshEnvy/mesh-forge/actions/runs/${build.githubRunId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-400 hover:text-cyan-300 underline font-semibold"
|
||||
title="Current run"
|
||||
>
|
||||
{build.githubRunId}
|
||||
</a>
|
||||
)}
|
||||
{build.githubRunIdHistory?.map(id => (
|
||||
<a
|
||||
key={id}
|
||||
href={`https://github.com/MeshEnvy/mesh-forge/actions/runs/${id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-400 hover:text-cyan-300 underline"
|
||||
>
|
||||
{id}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Actions */}
|
||||
{showActions && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{status === "success" && build.buildHash && (
|
||||
<BuildDownloadButton
|
||||
build={build}
|
||||
type={ArtifactType.Firmware}
|
||||
className="bg-cyan-600 hover:bg-cyan-700"
|
||||
compact={true}
|
||||
/>
|
||||
)}
|
||||
{build.buildHash && (
|
||||
<>
|
||||
<BuildDownloadButton
|
||||
build={build}
|
||||
type={ArtifactType.Source}
|
||||
variant="outline"
|
||||
className="bg-slate-700 hover:bg-slate-600"
|
||||
compact={true}
|
||||
/>
|
||||
<Button onClick={handleOpenBashModal} variant="outline" className="bg-slate-700 hover:bg-slate-600">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="m6.75 7.5l3 2.25l-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25" />
|
||||
</svg>
|
||||
Local Build Script (Bash)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isAdmin && build && onRetry && (
|
||||
<Button onClick={handleRetry} className="bg-cyan-600 hover:bg-cyan-700">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M11.896 18a.75.75 0 0 1-.75.75c-3.792 0-6.896-3.005-6.896-6.75s3.104-6.75 6.896-6.75c3.105 0 5.749 2.015 6.605 4.801l.603-1.02a.75.75 0 0 1 1.292.763l-1.63 2.755a.75.75 0 0 1-1.014.272L14.18 11.23a.75.75 0 1 1 .737-1.307l1.472.83c-.574-2.288-2.691-4.003-5.242-4.003C8.149 6.75 5.75 9.117 5.75 12s2.399 5.25 5.396 5.25a.75.75 0 0 1 .75.75" />
|
||||
</svg>
|
||||
Retry Build
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Messages */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Bash Script Modal */}
|
||||
{showBashModal && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
onClick={() => {
|
||||
setShowBashModal(false)
|
||||
setBashCopied(false)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-slate-900 border border-slate-700 rounded-lg shadow-xl max-w-3xl w-full mx-4 max-h-[80vh] flex flex-col"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-800">
|
||||
<h3 className="text-lg font-semibold text-white">Build Script</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowBashModal(false)
|
||||
setBashCopied(false)
|
||||
}}
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-auto flex-1">
|
||||
<pre className="bg-slate-950 border border-slate-800 rounded p-4 text-sm text-slate-300 font-mono overflow-x-auto">
|
||||
<code>{generateBashCommand()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 p-4 border-t border-slate-800">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowBashModal(false)
|
||||
setBashCopied(false)
|
||||
}}
|
||||
variant="outline"
|
||||
className="border-slate-600 hover:bg-slate-800"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={handleCopyBashFromModal} className="bg-cyan-600 hover:bg-cyan-700">
|
||||
{bashCopied ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy Script
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export function PluginCard(props: PluginCardProps) {
|
||||
e.stopPropagation()
|
||||
navigate(`/builds/new?plugin=${id}`)
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-cyan-400 bg-cyan-400/10 border border-cyan-400/20 rounded hover:bg-cyan-400/20 transition-colors"
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-cyan-400 bg-cyan-400/10 border border-cyan-400/20 rounded hover:bg-cyan-400/20 transition-colors cursor-pointer"
|
||||
>
|
||||
<Zap className="w-3 h-3" />
|
||||
Build Now
|
||||
|
||||
Reference in New Issue
Block a user