421 lines
16 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
} |