laconic-deployer-frontend/apps/deploy-fe/src/components/onboarding/deploy-step/deploy-step.tsx

421 lines
16 KiB
TypeScript

// src/components/onboarding/deploy-step/deploy-step.tsx
'use client'
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { Github, Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useGQLClient } from '@/context'
import { useWallet } from '@/context/WalletContext'
import { useDeployment } from '@/hooks/useDeployment'
import { useTemplateDeployment } from '@/hooks/useTemplate'
import { Button } from '@workspace/ui/components/button'
import { Progress } from '@workspace/ui/components/progress'
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter } from '@workspace/ui/components/dialog'
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
import { Badge } from '@workspace/ui/components/badge'
import { toast } from 'sonner'
export function DeployStep() {
const { previousStep, nextStep, formData, setFormData } = useOnboarding()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// State
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [deploymentError, setDeploymentError] = useState<string | null>(null)
const [deploymentSuccess, setDeploymentSuccess] = useState(false)
// Contexts and hooks
const { wallet } = useWallet()
const { deployRepository, isDeploying: isRepoDeploying } = useDeployment()
const { deployTemplate, isDeploying: isTemplateDeploying } = useTemplateDeployment()
// Determine deployment type and get the right deploying state
const isTemplateMode = formData.deploymentMode === 'template'
const isDeploying = isTemplateMode ? isTemplateDeploying : isRepoDeploying
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// Get deployment info
const getDeploymentInfo = () => {
if (isTemplateMode) {
return {
name: formData.template?.name || 'Template Project',
source: formData.template?.repoFullName || 'Unknown Template',
projectName: formData.projectName || 'New Project',
type: 'Template'
}
} else {
return {
name: formData.githubRepo?.split('/').pop() || 'Repository',
source: formData.githubRepo || 'Unknown Repository',
projectName: formData.projectName || formData.githubRepo?.split('/').pop() || 'New Project',
type: 'Repository'
}
}
}
const deploymentInfo = getDeploymentInfo()
// Open the confirmation modal
const handlePayAndDeploy = () => {
if (!wallet?.address) {
toast.error('Wallet not connected')
return
}
if (!formData.selectedOrg) {
toast.error('No organization selected')
return
}
if (isTemplateMode && (!formData.template || !formData.projectName)) {
toast.error('Template or project name missing')
return
}
if (!isTemplateMode && !formData.githubRepo) {
toast.error('Repository not selected')
return
}
setShowConfirmDialog(true)
}
// Close the confirmation modal
const handleCancelConfirm = () => {
setShowConfirmDialog(false)
}
// Handle confirmed deployment
const handleConfirmDeploy = async () => {
setShowConfirmDialog(false)
setDeploymentError(null)
setDeploymentSuccess(false)
try {
if (isTemplateMode) {
await deployTemplateProject()
} else {
await deployRepositoryProject()
}
} catch (error) {
console.error('Deployment failed:', error)
setDeploymentError(error instanceof Error ? error.message : 'Deployment failed')
}
}
// Deploy template project
const deployTemplateProject = async () => {
if (!formData.template || !formData.projectName || !formData.selectedOrg) {
throw new Error('Missing required template deployment data')
}
const config = {
template: formData.template,
projectName: formData.projectName,
organizationSlug: formData.selectedOrg,
environmentVariables: formData.environmentVariables || [],
deployerLrn: formData.selectedLrn
}
console.log('Deploying template with config:', config)
const result = await deployTemplate(config)
// Save deployment results
setFormData({
deploymentId: result.deploymentId,
deploymentUrl: result.deploymentUrl,
projectId: result.projectId,
repositoryUrl: result.repositoryUrl
})
setDeploymentSuccess(true)
toast.success('Template deployed successfully!')
// Move to success step after short delay
setTimeout(() => {
nextStep()
}, 2000)
}
// Deploy repository project
const deployRepositoryProject = async () => {
if (!formData.githubRepo || !formData.selectedOrg) {
throw new Error('Missing required repository deployment data')
}
const config = {
projectId: '', // Will be generated by backend
organizationSlug: formData.selectedOrg,
repository: formData.githubRepo,
branch: 'main', // Default branch
name: formData.projectName || formData.githubRepo.split('/').pop() || 'New Project',
environmentVariables: formData.environmentVariables || []
}
console.log('Deploying repository with config:', config)
const result = await deployRepository(config)
// Save deployment results
setFormData({
deploymentId: result.id,
deploymentUrl: result.url,
projectId: result.id
})
setDeploymentSuccess(true)
toast.success('Repository deployed successfully!')
// Move to success step after short delay
setTimeout(() => {
nextStep()
}, 2000)
}
// Don't render UI until after mount to prevent hydration mismatch
if (!mounted) {
return null
}
// Determine if dark mode is active
const isDarkMode = resolvedTheme === 'dark'
return (
<>
<div className="w-full h-full flex flex-col items-center justify-center p-8">
<div className="max-w-md w-full mx-auto">
{/* Deploy icon */}
<div className="mx-auto mb-6 flex justify-center">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="7.5 4.21 12 6.81 16.5 4.21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="7.5 19.79 7.5 14.6 3 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="21 12 16.5 14.6 16.5 19.79" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<line x1="12" y1="22.08" x2="12" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* Deploy header */}
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
{isDeploying ? 'Deploying...' : 'Deploy'}
</h2>
<p className="text-center text-zinc-500 mb-8">
{isDeploying
? 'Your project is being deployed. This may take a few minutes.'
: 'Review and confirm deployment'}
</p>
{/* Deployment Summary */}
<Card className="mb-6">
<CardHeader className="pb-3">
<CardTitle className="text-sm flex items-center justify-between">
Deployment Summary
<Badge variant="secondary">{deploymentInfo.type}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3">
<div className="flex items-center gap-3">
<Github className="h-4 w-4 text-muted-foreground" />
<div className="flex-1 min-w-0">
<div className="font-medium text-sm">{deploymentInfo.projectName}</div>
<div className="text-xs text-muted-foreground font-mono">
{deploymentInfo.source}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-2 text-xs">
<div>
<div className="text-muted-foreground">Organization</div>
<div className="font-medium">{formData.selectedOrg}</div>
</div>
<div>
<div className="text-muted-foreground">Deployer</div>
<div className="font-medium">{formData.selectedLrn ? 'LRN' : 'Auction'}</div>
</div>
</div>
{formData.environmentVariables && formData.environmentVariables.length > 0 && (
<div className="pt-2">
<div className="text-xs text-muted-foreground mb-1">Environment Variables</div>
<div className="text-xs">
{formData.environmentVariables.length} variable{formData.environmentVariables.length !== 1 ? 's' : ''} configured
</div>
</div>
)}
</CardContent>
</Card>
{/* Error Display */}
{deploymentError && (
<Alert className="mb-6" variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<div className="font-medium">Deployment Failed</div>
<div className="text-sm mt-1">{deploymentError}</div>
</AlertDescription>
</Alert>
)}
{/* Success Display */}
{deploymentSuccess && (
<Alert className="mb-6">
<CheckCircle2 className="h-4 w-4" />
<AlertDescription>
<div className="font-medium">Deployment Successful!</div>
<div className="text-sm mt-1">
Your project has been deployed successfully. You'll be redirected to the project dashboard.
</div>
</AlertDescription>
</Alert>
)}
{/* Deployment Progress - Only show while deploying */}
{isDeploying && (
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
<div className={`${isDarkMode ? "text-white" : "text-zinc-900"} text-sm`}>
{isTemplateMode ? 'Creating repository from template...' : 'Deploying repository...'}
</div>
</div>
<Progress value={undefined} className={`h-2 ${isDarkMode ? "bg-zinc-800" : "bg-zinc-200"}`} />
<div className="text-xs text-muted-foreground mt-2">
This process may take several minutes. Please do not close this window.
</div>
</div>
)}
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-4">
<Button
variant="outline"
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
onClick={previousStep}
disabled={isDeploying || deploymentSuccess}
>
Previous
</Button>
{deploymentSuccess ? (
<Button
className="bg-green-600 hover:bg-green-700 text-white"
onClick={nextStep}
>
<CheckCircle2 className="mr-2 h-4 w-4" />
Continue
</Button>
) : isDeploying ? (
<Button
className={`${isDarkMode ? "bg-zinc-700 text-zinc-300" : "bg-zinc-300 text-zinc-600"}`}
disabled
>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deploying...
</Button>
) : (
<Button
className="bg-blue-600 hover:bg-blue-700 text-white flex items-center"
onClick={handlePayAndDeploy}
disabled={deploymentError !== null}
>
{formData.deploymentType === 'auction' ? 'Pay and Deploy' : 'Deploy'}
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</Button>
)}
</div>
</div>
</div>
{/* Transaction Confirmation Dialog */}
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent className="bg-background border max-w-md">
<DialogTitle>Confirm Deployment</DialogTitle>
<DialogDescription>
Review the deployment details before proceeding.
</DialogDescription>
<div className="space-y-4 py-4">
{/* Project Info */}
<div className="space-y-2">
<h3 className="text-sm font-medium">Project Details</h3>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Name:</span>
<span>{deploymentInfo.projectName}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Type:</span>
<span>{deploymentInfo.type}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Source:</span>
<span className="font-mono text-xs">{deploymentInfo.source}</span>
</div>
</div>
</div>
{/* Wallet Info */}
<div className="space-y-2">
<h3 className="text-sm font-medium">Payment Address</h3>
<div className="p-2 bg-muted rounded text-xs font-mono break-all">
{wallet?.address}
</div>
</div>
{/* Deployer Info */}
{formData.selectedLrn && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Deployer</h3>
<div className="text-sm">
<div className="font-mono text-xs">{formData.selectedLrn}</div>
</div>
</div>
)}
{/* Cost Info */}
{formData.deploymentType === 'auction' && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Auction Details</h3>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Max Price:</span>
<span>{formData.maxPrice} aint</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Deployers:</span>
<span>{formData.deployerCount}</span>
</div>
</div>
</div>
)}
</div>
<DialogFooter className="flex justify-end space-x-2">
<Button
variant="outline"
onClick={handleCancelConfirm}
>
Cancel
</Button>
<Button
onClick={handleConfirmDeploy}
>
Confirm Deployment
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}