🤖 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>
|
|
)
|
|
} |