 c177363a19
			
		
	
	c177363a19
	
	
	
		
			
			🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			552 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			552 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 'use client'
 | |
| 
 | |
| import { useState, useEffect } from 'react'
 | |
| import { 
 | |
|   ServerStackIcon,
 | |
|   PlusIcon,
 | |
|   MagnifyingGlassIcon,
 | |
|   WifiIcon,
 | |
|   ComputerDesktopIcon,
 | |
|   ArrowPathIcon,
 | |
|   CheckCircleIcon,
 | |
|   ExclamationTriangleIcon,
 | |
|   InformationCircleIcon,
 | |
|   UserGroupIcon,
 | |
|   KeyIcon
 | |
| } from '@heroicons/react/24/outline'
 | |
| 
 | |
| interface DiscoveredNode {
 | |
|   id: string
 | |
|   hostname: string
 | |
|   ip: string
 | |
|   port: number
 | |
|   version: string
 | |
|   capabilities: string[]
 | |
|   status: 'online' | 'offline' | 'pending'
 | |
|   lastSeen: Date
 | |
| }
 | |
| 
 | |
| interface ClusterConfig {
 | |
|   mode: 'create' | 'join'
 | |
|   networkId: string
 | |
|   clusterName: string
 | |
|   nodeRole: 'coordinator' | 'worker' | 'hybrid'
 | |
|   joinKey?: string
 | |
|   targetNode?: string
 | |
|   autoDiscovery: boolean
 | |
|   encryption: boolean
 | |
|   redundancy: number
 | |
| }
 | |
| 
 | |
| interface ClusterFormationProps {
 | |
|   systemInfo: any
 | |
|   configData: any
 | |
|   onComplete: (data: any) => void
 | |
|   onBack?: () => void
 | |
|   isCompleted: boolean
 | |
| }
 | |
| 
 | |
| export default function ClusterFormation({ 
 | |
|   systemInfo, 
 | |
|   configData, 
 | |
|   onComplete, 
 | |
|   onBack, 
 | |
|   isCompleted 
 | |
| }: ClusterFormationProps) {
 | |
|   const [config, setConfig] = useState<ClusterConfig>({
 | |
|     mode: 'create',
 | |
|     networkId: '',
 | |
|     clusterName: '',
 | |
|     nodeRole: 'hybrid',
 | |
|     autoDiscovery: true,
 | |
|     encryption: true,
 | |
|     redundancy: 2
 | |
|   })
 | |
| 
 | |
|   const [discoveredNodes, setDiscoveredNodes] = useState<DiscoveredNode[]>([])
 | |
|   const [scanning, setScanning] = useState(false)
 | |
|   const [generatingKey, setGeneratingKey] = useState(false)
 | |
|   const [clusterKey, setClusterKey] = useState('')
 | |
| 
 | |
|   // Initialize configuration
 | |
|   useEffect(() => {
 | |
|     if (configData.cluster) {
 | |
|       setConfig(prev => ({ ...prev, ...configData.cluster }))
 | |
|     }
 | |
|     
 | |
|     // Generate default network ID based on hostname
 | |
|     if (!config.networkId && systemInfo?.network?.hostname) {
 | |
|       const hostname = systemInfo.network.hostname
 | |
|       const timestamp = Date.now().toString(36).slice(-4)
 | |
|       setConfig(prev => ({
 | |
|         ...prev,
 | |
|         networkId: `bzzz-${hostname}-${timestamp}`,
 | |
|         clusterName: `${hostname} BZZZ Cluster`
 | |
|       }))
 | |
|     }
 | |
|   }, [systemInfo, configData])
 | |
| 
 | |
|   // Auto-discover nodes when joining
 | |
|   useEffect(() => {
 | |
|     if (config.mode === 'join' && config.autoDiscovery) {
 | |
|       scanForNodes()
 | |
|     }
 | |
|   }, [config.mode, config.autoDiscovery])
 | |
| 
 | |
|   const scanForNodes = async () => {
 | |
|     setScanning(true)
 | |
|     try {
 | |
|       // This would be a real mDNS/network scan in production
 | |
|       // Simulating discovery for demo
 | |
|       await new Promise(resolve => setTimeout(resolve, 2000))
 | |
|       
 | |
|       const mockNodes: DiscoveredNode[] = [
 | |
|         {
 | |
|           id: 'node-001',
 | |
|           hostname: 'ironwood',
 | |
|           ip: '192.168.1.72',
 | |
|           port: 8080,
 | |
|           version: '2.0.0',
 | |
|           capabilities: ['coordinator', 'storage', 'compute'],
 | |
|           status: 'online',
 | |
|           lastSeen: new Date()
 | |
|         },
 | |
|         {
 | |
|           id: 'node-002',
 | |
|           hostname: 'walnut',
 | |
|           ip: '192.168.1.27',
 | |
|           port: 8080,
 | |
|           version: '2.0.0',
 | |
|           capabilities: ['worker', 'compute'],
 | |
|           status: 'online',
 | |
|           lastSeen: new Date()
 | |
|         }
 | |
|       ]
 | |
|       
 | |
|       setDiscoveredNodes(mockNodes)
 | |
|     } catch (error) {
 | |
|       console.error('Node discovery failed:', error)
 | |
|     } finally {
 | |
|       setScanning(false)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const generateClusterKey = async () => {
 | |
|     setGeneratingKey(true)
 | |
|     try {
 | |
|       // Generate a secure cluster key
 | |
|       const key = Array.from(crypto.getRandomValues(new Uint8Array(32)))
 | |
|         .map(b => b.toString(16).padStart(2, '0'))
 | |
|         .join('')
 | |
|       setClusterKey(key)
 | |
|     } catch (error) {
 | |
|       // Fallback key generation
 | |
|       const key = Math.random().toString(36).substr(2, 32)
 | |
|       setClusterKey(key)
 | |
|     } finally {
 | |
|       setGeneratingKey(false)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const getNodeRoleDescription = (role: string) => {
 | |
|     switch (role) {
 | |
|       case 'coordinator':
 | |
|         return 'Manages cluster state and coordinates tasks. Requires stable network connection.'
 | |
|       case 'worker':
 | |
|         return 'Executes tasks assigned by coordinators. Can be dynamically added/removed.'
 | |
|       case 'hybrid':
 | |
|         return 'Can act as both coordinator and worker. Recommended for most deployments.'
 | |
|       default:
 | |
|         return ''
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const getSystemRecommendation = () => {
 | |
|     const memoryGB = systemInfo?.memory_mb ? Math.round(systemInfo.memory_mb / 1024) : 8
 | |
|     const cpuCores = systemInfo?.cpu_cores || 4
 | |
|     const hasGPU = systemInfo?.gpus?.length > 0
 | |
| 
 | |
|     if (memoryGB >= 16 && cpuCores >= 8) {
 | |
|       return {
 | |
|         role: 'coordinator',
 | |
|         reason: 'High-performance system suitable for cluster coordination'
 | |
|       }
 | |
|     } else if (hasGPU) {
 | |
|       return {
 | |
|         role: 'hybrid',
 | |
|         reason: 'GPU acceleration available - good for both coordination and compute tasks'
 | |
|       }
 | |
|     } else {
 | |
|       return {
 | |
|         role: 'worker',
 | |
|         reason: 'Resource-optimized configuration for task execution'
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const handleSubmit = (e: React.FormEvent) => {
 | |
|     e.preventDefault()
 | |
|     
 | |
|     const clusterData = {
 | |
|       ...config,
 | |
|       clusterKey: config.mode === 'create' ? clusterKey : undefined,
 | |
|       systemInfo: {
 | |
|         hostname: systemInfo?.network?.hostname,
 | |
|         ip: systemInfo?.network?.private_ips?.[0],
 | |
|         capabilities: systemInfo?.gpus?.length > 0 ? ['compute', 'gpu'] : ['compute']
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     onComplete({ cluster: clusterData })
 | |
|   }
 | |
| 
 | |
|   const recommendation = getSystemRecommendation()
 | |
| 
 | |
|   return (
 | |
|     <form onSubmit={handleSubmit} className="space-y-6">
 | |
|       {/* Cluster Mode Selection */}
 | |
|       <div className="bg-gray-50 rounded-lg p-6">
 | |
|         <h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
 | |
|           <ServerStackIcon className="h-6 w-6 text-bzzz-primary mr-2" />
 | |
|           Cluster Mode
 | |
|         </h3>
 | |
|         
 | |
|         <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | |
|           <div
 | |
|             className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
 | |
|               config.mode === 'create'
 | |
|                 ? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
 | |
|                 : 'border-gray-200 hover:border-gray-300'
 | |
|             }`}
 | |
|             onClick={() => setConfig(prev => ({ ...prev, mode: 'create' }))}
 | |
|           >
 | |
|             <div className="flex items-center mb-2">
 | |
|               <PlusIcon className="h-5 w-5 text-bzzz-primary mr-2" />
 | |
|               <div className="font-medium text-gray-900">Create New Cluster</div>
 | |
|             </div>
 | |
|             <div className="text-sm text-gray-600">
 | |
|               Start a new BZZZ cluster and become the initial coordinator node.
 | |
|             </div>
 | |
|           </div>
 | |
|           
 | |
|           <div
 | |
|             className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
 | |
|               config.mode === 'join'
 | |
|                 ? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
 | |
|                 : 'border-gray-200 hover:border-gray-300'
 | |
|             }`}
 | |
|             onClick={() => setConfig(prev => ({ ...prev, mode: 'join' }))}
 | |
|           >
 | |
|             <div className="flex items-center mb-2">
 | |
|               <UserGroupIcon className="h-5 w-5 text-bzzz-primary mr-2" />
 | |
|               <div className="font-medium text-gray-900">Join Existing Cluster</div>
 | |
|             </div>
 | |
|             <div className="text-sm text-gray-600">
 | |
|               Connect to an existing BZZZ cluster as a worker or coordinator node.
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       {/* Create Cluster Configuration */}
 | |
|       {config.mode === 'create' && (
 | |
|         <div className="space-y-6">
 | |
|           <div className="bg-white border border-gray-200 rounded-lg p-6">
 | |
|             <h3 className="text-lg font-medium text-gray-900 mb-4">New Cluster Configuration</h3>
 | |
|             
 | |
|             <div className="space-y-4">
 | |
|               <div>
 | |
|                 <label className="label">Cluster Name</label>
 | |
|                 <input
 | |
|                   type="text"
 | |
|                   value={config.clusterName}
 | |
|                   onChange={(e) => setConfig(prev => ({ ...prev, clusterName: e.target.value }))}
 | |
|                   placeholder="My BZZZ Cluster"
 | |
|                   className="input-field"
 | |
|                   required
 | |
|                 />
 | |
|               </div>
 | |
| 
 | |
|               <div>
 | |
|                 <label className="label">Network ID</label>
 | |
|                 <input
 | |
|                   type="text"
 | |
|                   value={config.networkId}
 | |
|                   onChange={(e) => setConfig(prev => ({ ...prev, networkId: e.target.value }))}
 | |
|                   placeholder="bzzz-cluster-001"
 | |
|                   className="input-field"
 | |
|                   required
 | |
|                 />
 | |
|                 <p className="text-sm text-gray-600 mt-1">
 | |
|                   Unique identifier for your cluster network
 | |
|                 </p>
 | |
|               </div>
 | |
| 
 | |
|               <div>
 | |
|                 <label className="label">Cluster Security Key</label>
 | |
|                 <div className="flex space-x-2">
 | |
|                   <input
 | |
|                     type="text"
 | |
|                     value={clusterKey}
 | |
|                     onChange={(e) => setClusterKey(e.target.value)}
 | |
|                     placeholder="Click generate or enter custom key"
 | |
|                     className="input-field flex-1"
 | |
|                     readOnly={!clusterKey}
 | |
|                   />
 | |
|                   <button
 | |
|                     type="button"
 | |
|                     onClick={generateClusterKey}
 | |
|                     disabled={generatingKey}
 | |
|                     className="btn-outline whitespace-nowrap"
 | |
|                   >
 | |
|                     {generatingKey ? (
 | |
|                       <ArrowPathIcon className="h-4 w-4 animate-spin" />
 | |
|                     ) : (
 | |
|                       <>
 | |
|                         <KeyIcon className="h-4 w-4 mr-1" />
 | |
|                         Generate
 | |
|                       </>
 | |
|                     )}
 | |
|                   </button>
 | |
|                 </div>
 | |
|                 <p className="text-sm text-gray-600 mt-1">
 | |
|                   This key will be required for other nodes to join your cluster
 | |
|                 </p>
 | |
|               </div>
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       )}
 | |
| 
 | |
|       {/* Join Cluster Configuration */}
 | |
|       {config.mode === 'join' && (
 | |
|         <div className="space-y-6">
 | |
|           <div className="bg-white border border-gray-200 rounded-lg p-6">
 | |
|             <div className="flex items-center justify-between mb-4">
 | |
|               <h3 className="text-lg font-medium text-gray-900">Available Clusters</h3>
 | |
|               <button
 | |
|                 type="button"
 | |
|                 onClick={scanForNodes}
 | |
|                 disabled={scanning}
 | |
|                 className="btn-outline text-sm"
 | |
|               >
 | |
|                 {scanning ? (
 | |
|                   <>
 | |
|                     <ArrowPathIcon className="h-4 w-4 animate-spin mr-1" />
 | |
|                     Scanning...
 | |
|                   </>
 | |
|                 ) : (
 | |
|                   <>
 | |
|                     <MagnifyingGlassIcon className="h-4 w-4 mr-1" />
 | |
|                     Scan Network
 | |
|                   </>
 | |
|                 )}
 | |
|               </button>
 | |
|             </div>
 | |
| 
 | |
|             {discoveredNodes.length > 0 ? (
 | |
|               <div className="space-y-3">
 | |
|                 {discoveredNodes.map((node) => (
 | |
|                   <div
 | |
|                     key={node.id}
 | |
|                     className={`border rounded-lg p-4 cursor-pointer transition-all ${
 | |
|                       config.targetNode === node.id
 | |
|                         ? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
 | |
|                         : 'border-gray-200 hover:border-gray-300'
 | |
|                     }`}
 | |
|                     onClick={() => setConfig(prev => ({ ...prev, targetNode: node.id }))}
 | |
|                   >
 | |
|                     <div className="flex items-center justify-between">
 | |
|                       <div>
 | |
|                         <div className="flex items-center">
 | |
|                           <ComputerDesktopIcon className="h-5 w-5 text-gray-500 mr-2" />
 | |
|                           <span className="font-medium text-gray-900">{node.hostname}</span>
 | |
|                           <span className={`ml-2 status-indicator ${
 | |
|                             node.status === 'online' ? 'status-online' : 'status-offline'
 | |
|                           }`}>
 | |
|                             {node.status}
 | |
|                           </span>
 | |
|                         </div>
 | |
|                         <div className="text-sm text-gray-600 mt-1">
 | |
|                           {node.ip}:{node.port} • Version {node.version}
 | |
|                         </div>
 | |
|                         <div className="flex flex-wrap gap-1 mt-1">
 | |
|                           {node.capabilities.map((cap, index) => (
 | |
|                             <span key={index} className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs">
 | |
|                               {cap}
 | |
|                             </span>
 | |
|                           ))}
 | |
|                         </div>
 | |
|                       </div>
 | |
|                       <WifiIcon className="h-5 w-5 text-bzzz-primary" />
 | |
|                     </div>
 | |
|                   </div>
 | |
|                 ))}
 | |
|               </div>
 | |
|             ) : (
 | |
|               <div className="text-center py-8">
 | |
|                 <MagnifyingGlassIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
 | |
|                 <p className="text-gray-600">
 | |
|                   {scanning ? 'Scanning for BZZZ clusters...' : 'No clusters found. Click scan to search for available clusters.'}
 | |
|                 </p>
 | |
|               </div>
 | |
|             )}
 | |
| 
 | |
|             {config.targetNode && (
 | |
|               <div className="mt-4 pt-4 border-t border-gray-200">
 | |
|                 <label className="label">Cluster Join Key</label>
 | |
|                 <input
 | |
|                   type="password"
 | |
|                   value={config.joinKey || ''}
 | |
|                   onChange={(e) => setConfig(prev => ({ ...prev, joinKey: e.target.value }))}
 | |
|                   placeholder="Enter cluster security key"
 | |
|                   className="input-field"
 | |
|                   required
 | |
|                 />
 | |
|                 <p className="text-sm text-gray-600 mt-1">
 | |
|                   Enter the security key provided by the cluster administrator
 | |
|                 </p>
 | |
|               </div>
 | |
|             )}
 | |
|           </div>
 | |
|         </div>
 | |
|       )}
 | |
| 
 | |
|       {/* Node Role Configuration */}
 | |
|       <div className="bg-white border border-gray-200 rounded-lg p-6">
 | |
|         <h3 className="text-lg font-medium text-gray-900 mb-4">Node Role</h3>
 | |
|         
 | |
|         {/* System Recommendation */}
 | |
|         <div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
 | |
|           <div className="flex items-start">
 | |
|             <InformationCircleIcon className="h-5 w-5 text-blue-600 mr-2 mt-0.5" />
 | |
|             <div>
 | |
|               <div className="font-medium text-blue-800">
 | |
|                 Recommended: {recommendation.role.charAt(0).toUpperCase() + recommendation.role.slice(1)}
 | |
|               </div>
 | |
|               <div className="text-sm text-blue-700 mt-1">
 | |
|                 {recommendation.reason}
 | |
|               </div>
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div className="space-y-3">
 | |
|           {['coordinator', 'worker', 'hybrid'].map((role) => (
 | |
|             <div
 | |
|               key={role}
 | |
|               className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
 | |
|                 config.nodeRole === role
 | |
|                   ? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
 | |
|                   : 'border-gray-200 hover:border-gray-300'
 | |
|               }`}
 | |
|               onClick={() => setConfig(prev => ({ ...prev, nodeRole: role as any }))}
 | |
|             >
 | |
|               <div className="flex items-center">
 | |
|                 <input
 | |
|                   type="radio"
 | |
|                   name="nodeRole"
 | |
|                   value={role}
 | |
|                   checked={config.nodeRole === role}
 | |
|                   onChange={() => setConfig(prev => ({ ...prev, nodeRole: role as any }))}
 | |
|                   className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300"
 | |
|                 />
 | |
|                 <div className="ml-3">
 | |
|                   <div className="font-medium text-gray-900 capitalize">{role}</div>
 | |
|                   <div className="text-sm text-gray-600">{getNodeRoleDescription(role)}</div>
 | |
|                 </div>
 | |
|               </div>
 | |
|             </div>
 | |
|           ))}
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       {/* Advanced Options */}
 | |
|       <div className="bg-white border border-gray-200 rounded-lg p-6">
 | |
|         <h3 className="text-lg font-medium text-gray-900 mb-4">Advanced Options</h3>
 | |
|         
 | |
|         <div className="space-y-4">
 | |
|           <div className="flex items-center">
 | |
|             <input
 | |
|               type="checkbox"
 | |
|               id="autoDiscovery"
 | |
|               checked={config.autoDiscovery}
 | |
|               onChange={(e) => setConfig(prev => ({ ...prev, autoDiscovery: e.target.checked }))}
 | |
|               className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
 | |
|             />
 | |
|             <label htmlFor="autoDiscovery" className="ml-2 text-sm font-medium text-gray-700">
 | |
|               Enable automatic node discovery (mDNS)
 | |
|             </label>
 | |
|           </div>
 | |
| 
 | |
|           <div className="flex items-center">
 | |
|             <input
 | |
|               type="checkbox"
 | |
|               id="encryption"
 | |
|               checked={config.encryption}
 | |
|               onChange={(e) => setConfig(prev => ({ ...prev, encryption: e.target.checked }))}
 | |
|               className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
 | |
|             />
 | |
|             <label htmlFor="encryption" className="ml-2 text-sm font-medium text-gray-700">
 | |
|               Enable end-to-end encryption for cluster communication
 | |
|             </label>
 | |
|           </div>
 | |
| 
 | |
|           <div>
 | |
|             <label className="label">Redundancy Level</label>
 | |
|             <select
 | |
|               value={config.redundancy}
 | |
|               onChange={(e) => setConfig(prev => ({ ...prev, redundancy: parseInt(e.target.value) }))}
 | |
|               className="input-field"
 | |
|             >
 | |
|               <option value={1}>Low (1 replica)</option>
 | |
|               <option value={2}>Medium (2 replicas)</option>
 | |
|               <option value={3}>High (3 replicas)</option>
 | |
|             </select>
 | |
|             <p className="text-sm text-gray-600 mt-1">
 | |
|               Number of replicas for critical cluster data
 | |
|             </p>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       {/* Configuration Summary */}
 | |
|       <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
 | |
|         <div className="flex items-center mb-2">
 | |
|           <CheckCircleIcon className="h-5 w-5 text-blue-600 mr-2" />
 | |
|           <span className="text-blue-800 font-medium">Configuration Summary</span>
 | |
|         </div>
 | |
|         <div className="text-blue-700 text-sm space-y-1">
 | |
|           <p>• Mode: {config.mode === 'create' ? 'Create new cluster' : 'Join existing cluster'}</p>
 | |
|           <p>• Role: {config.nodeRole}</p>
 | |
|           <p>• Hostname: {systemInfo?.network?.hostname || 'Unknown'}</p>
 | |
|           <p>• IP Address: {systemInfo?.network?.private_ips?.[0] || 'Unknown'}</p>
 | |
|           {config.mode === 'create' && <p>• Cluster: {config.clusterName}</p>}
 | |
|           {config.encryption && <p>• Security: Encrypted communication enabled</p>}
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       {/* Action Buttons */}
 | |
|       <div className="flex justify-between pt-6 border-t border-gray-200">
 | |
|         <div>
 | |
|           {onBack && (
 | |
|             <button type="button" onClick={onBack} className="btn-outline">
 | |
|               Back
 | |
|             </button>
 | |
|           )}
 | |
|         </div>
 | |
|         
 | |
|         <button
 | |
|           type="submit"
 | |
|           disabled={
 | |
|             (config.mode === 'create' && (!config.clusterName || !config.networkId || !clusterKey)) ||
 | |
|             (config.mode === 'join' && (!config.targetNode || !config.joinKey))
 | |
|           }
 | |
|           className="btn-primary"
 | |
|         >
 | |
|           {isCompleted ? 'Continue' : 'Next: Testing & Validation'}
 | |
|         </button>
 | |
|       </div>
 | |
|     </form>
 | |
|   )
 | |
| } |