ai-wpa/app/services/kubernetes/page.tsx

643 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Header } from '@/components/header'
import { Footer } from '@/components/footer'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Container,
Server,
HardDrive,
Shield,
AlertCircle,
Check,
Plus,
Minus,
Cpu,
GitBranch,
Download,
ExternalLink,
} from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
interface NodePool {
id: string
name: string
size: string
count: number
autoscale: boolean
minNodes?: number
maxNodes?: number
}
interface ClusterSize {
id: string
name: string
price: {
daily: number
monthly: number
}
}
interface DeploymentResult {
status: string
clusterId?: string
message?: string
id?: string
error?: string
[key: string]: any
}
export default function KubernetesPage() {
const router = useRouter()
const { user } = useAuth()
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
const [billingCycle, setBillingCycle] = useState<'daily' | 'monthly'>('monthly')
// Form state
const [clusterName, setClusterName] = useState('')
const [k8sVersion, setK8sVersion] = useState('1.30.0-utho')
const [nodePools, setNodePools] = useState<NodePool[]>([
{
id: '1',
name: 'default-pool',
size: '10215',
count: 2,
autoscale: false,
minNodes: 2,
maxNodes: 4,
},
])
// Available cluster sizes
const clusterSizes: ClusterSize[] = [
{ id: '10215', name: '2 vCPU, 4GB RAM', price: { daily: 200, monthly: 4000 } },
{ id: '10216', name: '4 vCPU, 8GB RAM', price: { daily: 300, monthly: 7000 } },
{ id: '10217', name: '8 vCPU, 16GB RAM', price: { daily: 500, monthly: 12000 } },
{ id: '10218', name: '16 vCPU, 32GB RAM', price: { daily: 800, monthly: 20000 } },
]
// Kubernetes versions
const k8sVersions = ['1.30.0-utho', '1.29.0-utho', '1.28.0-utho']
// Mock user balance
const userBalance = 15000
// Calculate total price
const calculatePrice = () => {
let total = 0
nodePools.forEach((pool) => {
const size = clusterSizes.find((s) => s.id === pool.size)
if (size) {
total += size.price[billingCycle] * pool.count
}
})
return total
}
// Add node pool
const addNodePool = () => {
const newPool: NodePool = {
id: Date.now().toString(),
name: `pool-${nodePools.length + 1}`,
size: '10215',
count: 1,
autoscale: false,
minNodes: 1,
maxNodes: 3,
}
setNodePools([...nodePools, newPool])
}
// Remove node pool
const removeNodePool = (id: string) => {
if (nodePools.length > 1) {
setNodePools(nodePools.filter((pool) => pool.id !== id))
}
}
// Update node pool
const updateNodePool = (id: string, field: keyof NodePool, value: any) => {
setNodePools(nodePools.map((pool) => (pool.id === id ? { ...pool, [field]: value } : pool)))
}
// Format node pools for API
const formatNodePools = () => {
return nodePools.map((pool) => ({
name: pool.name,
size: pool.size,
count: pool.count,
autoscale: pool.autoscale,
min_nodes: pool.minNodes || pool.count,
max_nodes: pool.maxNodes || pool.count + 2,
}))
}
const handleDeploy = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setSuccess('')
setDeploymentResult(null)
if (!user) {
router.push('/auth?redirect=/services/kubernetes')
return
}
// Validate form
if (!clusterName) {
setError('Cluster name is required')
return
}
// Validate cluster name
if (!/^[a-z0-9-]+$/.test(clusterName)) {
setError('Cluster name can only contain lowercase letters, numbers, and hyphens')
return
}
// Check balance
const totalPrice = calculatePrice()
if (userBalance < totalPrice) {
setError(
`Insufficient balance. You need ₹${totalPrice.toFixed(2)} but only have ₹${userBalance.toFixed(2)}`
)
return
}
setLoading(true)
try {
const response = await fetch('/api/services/deploy-kubernetes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cluster_label: clusterName,
cluster_version: k8sVersion,
nodepools: formatNodePools(),
cycle: billingCycle,
amount: totalPrice,
}),
})
const result: DeploymentResult = await response.json()
if (!response.ok) {
throw new Error(result.message || `Deployment failed with status ${response.status}`)
}
if (result.status === 'success') {
setDeploymentResult(result)
setSuccess(
`Kubernetes cluster "${clusterName}" deployed successfully! Cluster ID: ${result.clusterId || result.id}`
)
// Reset form after successful deployment
setClusterName('')
setNodePools([
{
id: '1',
name: 'default-pool',
size: '10215',
count: 2,
autoscale: false,
minNodes: 2,
maxNodes: 4,
},
])
} else {
throw new Error(result.message || 'Deployment failed')
}
} catch (err) {
console.error('Deployment error:', err)
setError(err instanceof Error ? err.message : 'Failed to deploy Kubernetes cluster')
} finally {
setLoading(false)
}
}
const downloadKubeconfig = async (clusterId: string) => {
try {
const response = await fetch(`/api/services/deploy-kubernetes?clusterId=${clusterId}`)
if (response.ok) {
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = `kubeconfig-${clusterId}.yaml`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} else {
const errorData = await response.json()
setError(errorData.message || 'Failed to download kubeconfig')
}
} catch (err) {
console.error('Kubeconfig download error:', err)
setError('Failed to download kubeconfig')
}
}
const resetForm = () => {
setDeploymentResult(null)
setSuccess('')
setError('')
}
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 pt-24 pb-16">
<div className="max-w-5xl mx-auto">
{/* Page Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
<Container className="w-8 h-8 text-white" />
</div>
<h1 className="text-4xl font-bold mb-4">Kubernetes Edge</h1>
<p className="text-xl text-muted-foreground">
Managed Kubernetes clusters with auto-scaling and enterprise-grade security
</p>
</div>
{!user && (
<Alert className="mb-8">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
You need to be logged in to deploy a Kubernetes cluster.{' '}
<a href="/auth?redirect=/services/kubernetes" className="text-primary underline">
Click here to login
</a>
</AlertDescription>
</Alert>
)}
{deploymentResult ? (
<Card>
<CardHeader>
<CardTitle>Deployment Successful!</CardTitle>
<CardDescription>
Your Kubernetes cluster has been deployed successfully.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Cluster ID</Label>
<p className="text-sm font-mono bg-muted p-2 rounded">
{deploymentResult.clusterId || deploymentResult.id}
</p>
</div>
<div>
<Label>Status</Label>
<p className="text-sm capitalize">{deploymentResult.status}</p>
</div>
</div>
<div className="flex gap-4">
{deploymentResult.clusterId && (
<Button onClick={() => downloadKubeconfig(deploymentResult.clusterId!)}>
<Download className="w-4 h-4 mr-2" />
Download Kubeconfig
</Button>
)}
<Button variant="outline" onClick={() => router.push('/dashboard/clusters')}>
View Clusters
</Button>
<Button variant="outline" onClick={resetForm}>
Deploy Another
</Button>
</div>
</CardContent>
</Card>
) : (
<form onSubmit={handleDeploy}>
<div className="space-y-8">
{/* Billing Cycle */}
<Card>
<CardHeader>
<CardTitle>Billing Cycle</CardTitle>
<CardDescription>Choose your preferred billing period</CardDescription>
</CardHeader>
<CardContent>
<Tabs
value={billingCycle}
onValueChange={(v) => setBillingCycle(v as 'daily' | 'monthly')}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="daily">Daily</TabsTrigger>
<TabsTrigger value="monthly">Monthly (Save 25%)</TabsTrigger>
</TabsList>
</Tabs>
</CardContent>
</Card>
{/* Cluster Configuration */}
<Card>
<CardHeader>
<CardTitle>Cluster Configuration</CardTitle>
<CardDescription>Configure your Kubernetes cluster settings</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="clusterName">Cluster Name</Label>
<Input
id="clusterName"
placeholder="my-k8s-cluster"
value={clusterName}
onChange={(e) => setClusterName(e.target.value.toLowerCase())}
pattern="^[a-z0-9-]+$"
required
/>
<p className="text-xs text-muted-foreground mt-1">
Only lowercase letters, numbers, and hyphens allowed
</p>
</div>
<div>
<Label htmlFor="k8sVersion">Kubernetes Version</Label>
<Select value={k8sVersion} onValueChange={setK8sVersion}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{k8sVersions.map((version) => (
<SelectItem key={version} value={version}>
<div className="flex items-center gap-2">
<GitBranch className="w-4 h-4" />v{version.replace('-utho', '')}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="p-3 bg-blue-50 rounded-md border border-blue-200">
<p className="text-sm text-blue-700">
<strong>Location:</strong> Mumbai, India (Fixed)
</p>
<p className="text-xs text-blue-600 mt-1">
All Kubernetes clusters are deployed in our Mumbai datacenter for optimal
performance.
</p>
</div>
</CardContent>
</Card>
{/* Node Pools */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
Node Pools
<Button type="button" size="sm" onClick={addNodePool} variant="outline">
<Plus className="w-4 h-4 mr-1" />
Add Pool
</Button>
</CardTitle>
<CardDescription>Configure worker node pools for your cluster</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{nodePools.map((pool, index) => (
<Card key={pool.id}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<Input
value={pool.name}
onChange={(e) => updateNodePool(pool.id, 'name', e.target.value)}
className="max-w-xs"
placeholder="Pool name"
required
/>
{nodePools.length > 1 && (
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => removeNodePool(pool.id)}
>
<Minus className="w-4 h-4" />
</Button>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Node Size</Label>
<Select
value={pool.size}
onValueChange={(value) => updateNodePool(pool.id, 'size', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{clusterSizes.map((size) => (
<SelectItem key={size.id} value={size.id}>
<div className="flex items-center justify-between w-full">
<span>{size.name}</span>
<span className="ml-4 text-primary text-sm">
{size.price[billingCycle]}/
{billingCycle === 'daily' ? 'day' : 'mo'}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Node Count</Label>
<div className="flex items-center gap-2">
<Button
type="button"
size="sm"
variant="outline"
onClick={() =>
updateNodePool(pool.id, 'count', Math.max(1, pool.count - 1))
}
>
<Minus className="w-4 h-4" />
</Button>
<Input
type="number"
value={pool.count}
onChange={(e) =>
updateNodePool(pool.id, 'count', parseInt(e.target.value) || 1)
}
className="w-20 text-center"
min="1"
max="10"
/>
<Button
type="button"
size="sm"
variant="outline"
onClick={() =>
updateNodePool(pool.id, 'count', Math.min(10, pool.count + 1))
}
>
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
</div>
<div className="text-sm text-muted-foreground">
Pool cost:
{(clusterSizes.find((s) => s.id === pool.size)?.price[billingCycle] ||
0) * pool.count}
/{billingCycle === 'daily' ? 'day' : 'month'}
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
{/* Pricing Summary */}
<Card>
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{nodePools.map((pool) => {
const size = clusterSizes.find((s) => s.id === pool.size)
const cost = (size?.price[billingCycle] || 0) * pool.count
return (
<div key={pool.id} className="flex justify-between text-sm">
<span className="text-muted-foreground">
{pool.name} ({pool.count} nodes × {size?.name})
</span>
<span>{cost}</span>
</div>
)
})}
<div className="border-t pt-2">
<div className="flex justify-between font-semibold">
<span>Total</span>
<span className="text-primary">
{calculatePrice()}/{billingCycle === 'daily' ? 'day' : 'month'}
</span>
</div>
</div>
<div className="pt-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Your Balance</span>
<span
className={
userBalance >= calculatePrice() ? 'text-green-600' : 'text-red-600'
}
>
{userBalance.toFixed(2)}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Deploy Button */}
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Deploying Cluster...
</>
) : (
'Deploy Kubernetes Cluster'
)}
</Button>
</div>
</form>
)}
{/* Features */}
{!deploymentResult && (
<div className="mt-16 grid md:grid-cols-4 gap-6">
<Card>
<CardContent className="pt-6">
<div className="text-center">
<Cpu className="w-8 h-8 text-primary mx-auto mb-3" />
<h4 className="font-semibold mb-2">Auto-scaling</h4>
<p className="text-sm text-muted-foreground">
Automatically scale nodes based on workload
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-center">
<Shield className="w-8 h-8 text-primary mx-auto mb-3" />
<h4 className="font-semibold mb-2">Secure by Default</h4>
<p className="text-sm text-muted-foreground">
RBAC, network policies, and encryption
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-center">
<Server className="w-8 h-8 text-primary mx-auto mb-3" />
<h4 className="font-semibold mb-2">Load Balancing</h4>
<p className="text-sm text-muted-foreground">
Built-in load balancers for your services
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-center">
<HardDrive className="w-8 h-8 text-primary mx-auto mb-3" />
<h4 className="font-semibold mb-2">Persistent Storage</h4>
<p className="text-sm text-muted-foreground">SSD-backed persistent volumes</p>
</div>
</CardContent>
</Card>
</div>
)}
</div>
</div>
<Footer />
</div>
)
}