Save current BZZZ config-ui state before CHORUS branding update
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
ServerIcon,
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
PlayIcon,
|
||||
StopIcon,
|
||||
TrashIcon,
|
||||
DocumentTextIcon,
|
||||
ArrowPathIcon,
|
||||
CloudArrowDownIcon,
|
||||
Cog6ToothIcon,
|
||||
XMarkIcon,
|
||||
ComputerDesktopIcon
|
||||
} from '@heroicons/react/24/outline'
|
||||
|
||||
interface Machine {
|
||||
id: string
|
||||
hostname: string
|
||||
ip: string
|
||||
os: string
|
||||
osVersion: string
|
||||
sshStatus: 'unknown' | 'connected' | 'failed' | 'testing'
|
||||
deployStatus: 'not_deployed' | 'installing' | 'running' | 'stopped' | 'error'
|
||||
selected: boolean
|
||||
lastSeen?: string
|
||||
deployProgress?: number
|
||||
deployStep?: string
|
||||
systemInfo?: {
|
||||
cpu: number
|
||||
memory: number
|
||||
disk: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ServiceDeploymentProps {
|
||||
systemInfo: any
|
||||
@@ -17,32 +51,698 @@ export default function ServiceDeployment({
|
||||
onBack,
|
||||
isCompleted
|
||||
}: ServiceDeploymentProps) {
|
||||
const [machines, setMachines] = useState<Machine[]>([])
|
||||
const [isDiscovering, setIsDiscovering] = useState(false)
|
||||
const [discoveryProgress, setDiscoveryProgress] = useState(0)
|
||||
const [discoveryStatus, setDiscoveryStatus] = useState('')
|
||||
const [showLogs, setShowLogs] = useState<string | null>(null)
|
||||
const [deploymentLogs, setDeploymentLogs] = useState<{[key: string]: string[]}>({})
|
||||
const [showConsole, setShowConsole] = useState<string | null>(null)
|
||||
const [consoleLogs, setConsoleLogs] = useState<{[key: string]: string[]}>({})
|
||||
|
||||
const [config, setConfig] = useState({
|
||||
deploymentMethod: 'systemd',
|
||||
autoStart: true
|
||||
autoStart: true,
|
||||
healthCheckInterval: 30,
|
||||
selectedMachines: [] as string[]
|
||||
})
|
||||
|
||||
// Initialize with current machine
|
||||
useEffect(() => {
|
||||
const currentMachine: Machine = {
|
||||
id: 'localhost',
|
||||
hostname: systemInfo?.network?.hostname || 'localhost',
|
||||
ip: configData?.network?.primaryIP || '127.0.0.1',
|
||||
os: systemInfo?.os || 'linux',
|
||||
osVersion: 'Current Host',
|
||||
sshStatus: 'connected',
|
||||
deployStatus: 'running', // Already running since we're in setup
|
||||
selected: true,
|
||||
systemInfo: {
|
||||
cpu: systemInfo?.cpu_cores || 0,
|
||||
memory: Math.round((systemInfo?.memory_mb || 0) / 1024),
|
||||
disk: systemInfo?.storage?.free_space_gb || 0
|
||||
}
|
||||
}
|
||||
setMachines([currentMachine])
|
||||
setConfig(prev => ({ ...prev, selectedMachines: ['localhost'] }))
|
||||
}, [systemInfo, configData])
|
||||
|
||||
const discoverMachines = async () => {
|
||||
setIsDiscovering(true)
|
||||
setDiscoveryProgress(0)
|
||||
setDiscoveryStatus('Initializing network scan...')
|
||||
|
||||
try {
|
||||
// Simulate progress updates during discovery
|
||||
const progressInterval = setInterval(() => {
|
||||
setDiscoveryProgress(prev => {
|
||||
const newProgress = prev + 10
|
||||
if (newProgress <= 30) {
|
||||
setDiscoveryStatus('Scanning network subnet...')
|
||||
} else if (newProgress <= 60) {
|
||||
setDiscoveryStatus('Checking SSH accessibility...')
|
||||
} else if (newProgress <= 90) {
|
||||
setDiscoveryStatus('Gathering system information...')
|
||||
} else {
|
||||
setDiscoveryStatus('Finalizing discovery...')
|
||||
}
|
||||
return Math.min(newProgress, 95)
|
||||
})
|
||||
}, 200)
|
||||
|
||||
const response = await fetch('/api/setup/discover-machines', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
subnet: configData?.network?.allowedIPs?.[0] || '192.168.1.0/24',
|
||||
sshKey: configData?.security?.sshPublicKey
|
||||
})
|
||||
})
|
||||
|
||||
clearInterval(progressInterval)
|
||||
setDiscoveryProgress(100)
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
setDiscoveryStatus(`Found ${result.machines?.length || 0} machines`)
|
||||
|
||||
const discoveredMachines: Machine[] = result.machines.map((m: any) => ({
|
||||
id: m.ip,
|
||||
hostname: m.hostname || 'Unknown',
|
||||
ip: m.ip,
|
||||
os: m.os || 'unknown',
|
||||
osVersion: m.os_version || 'Unknown',
|
||||
sshStatus: 'unknown',
|
||||
deployStatus: 'not_deployed',
|
||||
selected: false,
|
||||
lastSeen: new Date().toISOString(),
|
||||
systemInfo: m.system_info
|
||||
}))
|
||||
|
||||
// Merge with existing machines (keep localhost)
|
||||
setMachines(prev => {
|
||||
const localhost = prev.find(m => m.id === 'localhost')
|
||||
return localhost ? [localhost, ...discoveredMachines] : discoveredMachines
|
||||
})
|
||||
} else {
|
||||
setDiscoveryStatus('Discovery failed - check network configuration')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Discovery failed:', error)
|
||||
setDiscoveryStatus('Discovery error - network unreachable')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsDiscovering(false)
|
||||
setDiscoveryProgress(0)
|
||||
setDiscoveryStatus('')
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const testSSHConnection = async (machineId: string) => {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? { ...m, sshStatus: 'testing' } : m
|
||||
))
|
||||
|
||||
try {
|
||||
const machine = machines.find(m => m.id === machineId)
|
||||
const response = await fetch('/api/setup/test-ssh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ip: machine?.ip,
|
||||
sshKey: configData?.security?.sshPrivateKey,
|
||||
sshUsername: configData?.security?.sshUsername || 'ubuntu',
|
||||
sshPassword: configData?.security?.sshPassword,
|
||||
sshPort: configData?.security?.sshPort || 22
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
sshStatus: result.success ? 'connected' : 'failed',
|
||||
os: result.os || m.os,
|
||||
osVersion: result.os_version || m.osVersion,
|
||||
systemInfo: result.system_info || m.systemInfo
|
||||
} : m
|
||||
))
|
||||
} catch (error) {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? { ...m, sshStatus: 'failed' } : m
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const deployToMachine = async (machineId: string) => {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
deployStatus: 'installing',
|
||||
deployProgress: 0,
|
||||
deployStep: 'Initializing deployment...'
|
||||
} : m
|
||||
))
|
||||
|
||||
const logs: string[] = []
|
||||
const consoleLogs: string[] = [`🚀 Starting deployment to ${machines.find(m => m.id === machineId)?.hostname} (${machines.find(m => m.id === machineId)?.ip})`]
|
||||
setDeploymentLogs(prev => ({ ...prev, [machineId]: logs }))
|
||||
setConsoleLogs(prev => ({ ...prev, [machineId]: consoleLogs }))
|
||||
|
||||
// Open console if not already showing
|
||||
if (!showConsole) {
|
||||
setShowConsole(machineId)
|
||||
}
|
||||
|
||||
// Real-time console logging helper
|
||||
const addConsoleLog = (message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
const logMessage = `[${timestamp}] ${message}`
|
||||
setConsoleLogs(prev => ({
|
||||
...prev,
|
||||
[machineId]: [...(prev[machineId] || []), logMessage]
|
||||
}))
|
||||
}
|
||||
|
||||
// Simulate progress updates
|
||||
const progressSteps = [
|
||||
{ progress: 10, step: 'Establishing SSH connection...' },
|
||||
{ progress: 30, step: 'Copying BZZZ binary...' },
|
||||
{ progress: 60, step: 'Creating systemd service...' },
|
||||
{ progress: 80, step: 'Starting service...' },
|
||||
{ progress: 100, step: 'Deployment complete!' }
|
||||
]
|
||||
|
||||
const updateProgress = (stepIndex: number) => {
|
||||
if (stepIndex < progressSteps.length) {
|
||||
const { progress, step } = progressSteps[stepIndex]
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
deployProgress: progress,
|
||||
deployStep: step
|
||||
} : m
|
||||
))
|
||||
logs.push(`📦 ${step}`)
|
||||
addConsoleLog(`📦 ${step}`)
|
||||
setDeploymentLogs(prev => ({ ...prev, [machineId]: [...(prev[machineId] || []), `📦 ${step}`] }))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const machine = machines.find(m => m.id === machineId)
|
||||
addConsoleLog(`🚀 Starting deployment to ${machine?.hostname}...`)
|
||||
addConsoleLog(`📡 Sending deployment request to backend API...`)
|
||||
|
||||
// Set initial progress
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
deployProgress: 10,
|
||||
deployStep: 'Contacting backend API...'
|
||||
} : m
|
||||
))
|
||||
const response = await fetch('/api/setup/deploy-service', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ip: machine?.ip,
|
||||
sshKey: configData?.security?.sshPrivateKey,
|
||||
sshUsername: configData?.security?.sshUsername || 'ubuntu',
|
||||
sshPassword: configData?.security?.sshPassword,
|
||||
sshPort: configData?.security?.sshPort || 22,
|
||||
config: {
|
||||
ports: {
|
||||
api: configData?.network?.bzzzPort || 8080,
|
||||
mcp: configData?.network?.mcpPort || 3000,
|
||||
webui: configData?.network?.webUIPort || 8080,
|
||||
p2p: configData?.network?.p2pPort || 7000
|
||||
},
|
||||
security: configData?.security,
|
||||
autoStart: config.autoStart
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
addConsoleLog(`📨 Received response from backend API`)
|
||||
|
||||
if (result.success) {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
deployStatus: 'running',
|
||||
deployProgress: 100,
|
||||
deployStep: 'Running'
|
||||
} : m
|
||||
))
|
||||
logs.push('✅ Deployment completed successfully')
|
||||
addConsoleLog('✅ Deployment completed successfully!')
|
||||
|
||||
// Show actual backend steps if provided
|
||||
if (result.steps) {
|
||||
result.steps.forEach((step: string) => {
|
||||
logs.push(step)
|
||||
addConsoleLog(`📋 ${step}`)
|
||||
})
|
||||
}
|
||||
addConsoleLog(`🎉 CHORUS:agents service is now running on ${machine?.hostname}`)
|
||||
} else {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
deployStatus: 'error',
|
||||
deployProgress: 0,
|
||||
deployStep: 'Failed'
|
||||
} : m
|
||||
))
|
||||
logs.push(`❌ Deployment failed: ${result.error}`)
|
||||
addConsoleLog(`❌ Deployment failed: ${result.error}`)
|
||||
addConsoleLog(`💡 Note: This was a real backend error, not simulated progress`)
|
||||
}
|
||||
} catch (error) {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? {
|
||||
...m,
|
||||
deployStatus: 'error',
|
||||
deployProgress: 0,
|
||||
deployStep: 'Error'
|
||||
} : m
|
||||
))
|
||||
logs.push(`❌ Deployment error: ${error}`)
|
||||
addConsoleLog(`❌ Deployment error: ${error}`)
|
||||
}
|
||||
|
||||
setDeploymentLogs(prev => ({ ...prev, [machineId]: logs }))
|
||||
}
|
||||
|
||||
const toggleMachineSelection = (machineId: string) => {
|
||||
setMachines(prev => prev.map(m =>
|
||||
m.id === machineId ? { ...m, selected: !m.selected } : m
|
||||
))
|
||||
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
selectedMachines: machines
|
||||
.map(m => m.id === machineId ? { ...m, selected: !m.selected } : m)
|
||||
.filter(m => m.selected)
|
||||
.map(m => m.id)
|
||||
}))
|
||||
}
|
||||
|
||||
const deployToSelected = async () => {
|
||||
const selectedMachines = machines.filter(m => m.selected && m.sshStatus === 'connected')
|
||||
for (const machine of selectedMachines) {
|
||||
if (machine.deployStatus === 'not_deployed') {
|
||||
await deployToMachine(machine.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeMachine = (machineId: string) => {
|
||||
// Don't allow removing localhost
|
||||
if (machineId === 'localhost') return
|
||||
|
||||
setMachines(prev => prev.filter(m => m.id !== machineId))
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
selectedMachines: prev.selectedMachines.filter(id => id !== machineId)
|
||||
}))
|
||||
|
||||
// Clean up logs for removed machine
|
||||
setDeploymentLogs(prev => {
|
||||
const { [machineId]: removed, ...rest } = prev
|
||||
return rest
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'connected': return <CheckCircleIcon className="h-5 w-5 text-green-500" />
|
||||
case 'failed': return <XCircleIcon className="h-5 w-5 text-red-500" />
|
||||
case 'testing': return <ArrowPathIcon className="h-5 w-5 text-blue-500 animate-spin" />
|
||||
case 'running': return <CheckCircleIcon className="h-5 w-5 text-green-500" />
|
||||
case 'installing': return <ArrowPathIcon className="h-5 w-5 text-blue-500 animate-spin" />
|
||||
case 'error': return <XCircleIcon className="h-5 w-5 text-red-500" />
|
||||
case 'stopped': return <StopIcon className="h-5 w-5 text-yellow-500" />
|
||||
default: return <ServerIcon className="h-5 w-5 text-gray-400" />
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
onComplete({ deployment: config })
|
||||
onComplete({
|
||||
deployment: {
|
||||
...config,
|
||||
machines: machines.filter(m => m.selected).map(m => ({
|
||||
id: m.id,
|
||||
ip: m.ip,
|
||||
hostname: m.hostname,
|
||||
deployStatus: m.deployStatus
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="text-center py-12">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Service Deployment
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Deploy and configure BZZZ services with monitoring and health checks.
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-yellow-800">
|
||||
This component is under development. Service deployment will be implemented here.
|
||||
|
||||
{/* OS Support Caution */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<div className="flex items-start">
|
||||
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-yellow-800">Operating System Support</h3>
|
||||
<p className="text-sm text-yellow-700 mt-1">
|
||||
CHORUS:agents automated deployment supports <strong>Linux distributions that use systemd by default</strong> (Ubuntu 16+, CentOS 7+, Debian 8+, RHEL 7+, etc.).
|
||||
For other operating systems or init systems, you'll need to manually deploy the CHORUS:agents binary and configure services on your cluster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network Discovery */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
||||
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
||||
Machine Discovery
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={discoverMachines}
|
||||
disabled={isDiscovering}
|
||||
className="btn-outline flex items-center"
|
||||
>
|
||||
<ArrowPathIcon className={`h-4 w-4 mr-2 ${isDiscovering ? 'animate-spin' : ''}`} />
|
||||
{isDiscovering ? 'Discovering...' : 'Discover Machines'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Scan network subnet: {configData?.network?.allowedIPs?.[0] || '192.168.1.0/24'}
|
||||
</p>
|
||||
|
||||
{/* Discovery Progress */}
|
||||
{isDiscovering && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">{discoveryStatus}</span>
|
||||
<span className="text-sm text-gray-500">{discoveryProgress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-bzzz-primary h-2 rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${discoveryProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Machine Table */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Cluster Machines</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={deployToSelected}
|
||||
disabled={machines.filter(m => m.selected && m.sshStatus === 'connected').length === 0}
|
||||
className="btn-primary flex items-center"
|
||||
>
|
||||
<CloudArrowDownIcon className="h-4 w-4 mr-2" />
|
||||
Deploy to Selected
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Select
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Machine
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Operating System
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
IP Address
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
SSH Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Deploy Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Remove
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{machines.map((machine) => (
|
||||
<tr key={machine.id} className={machine.selected ? 'bg-blue-50' : ''}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={machine.selected}
|
||||
onChange={() => toggleMachineSelection(machine.id)}
|
||||
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{machine.hostname}</div>
|
||||
{machine.systemInfo && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{machine.systemInfo.cpu} cores • {machine.systemInfo.memory}GB RAM • {machine.systemInfo.disk}GB disk
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{machine.os}</div>
|
||||
<div className="text-xs text-gray-500">{machine.osVersion}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{machine.ip}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
{getStatusIcon(machine.sshStatus)}
|
||||
<span className="ml-2 text-sm text-gray-900 capitalize">
|
||||
{machine.sshStatus.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
{getStatusIcon(machine.deployStatus)}
|
||||
<div className="ml-2 flex-1">
|
||||
<div className="text-sm text-gray-900 capitalize">
|
||||
{machine.deployStatus.replace('_', ' ')}
|
||||
</div>
|
||||
{machine.deployStatus === 'installing' && (
|
||||
<div className="mt-1">
|
||||
<div className="text-xs text-gray-500 mb-1">
|
||||
{machine.deployStep || 'Deploying...'}
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${machine.deployProgress || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{machine.deployProgress || 0}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
||||
{machine.id !== 'localhost' && machine.sshStatus !== 'connected' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => testSSHConnection(machine.id)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
disabled={machine.sshStatus === 'testing'}
|
||||
>
|
||||
Test SSH
|
||||
</button>
|
||||
)}
|
||||
|
||||
{machine.sshStatus === 'connected' && machine.deployStatus === 'not_deployed' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deployToMachine(machine.id)}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
|
||||
{machine.deployStatus !== 'not_deployed' && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLogs(machine.id)}
|
||||
className="text-gray-600 hover:text-gray-900 mr-2"
|
||||
title="View deployment logs"
|
||||
>
|
||||
<DocumentTextIcon className="h-4 w-4 inline" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConsole(machine.id)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
title="Open deployment console"
|
||||
>
|
||||
<ComputerDesktopIcon className="h-4 w-4 inline" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
{machine.id !== 'localhost' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeMachine(machine.id)}
|
||||
className="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50"
|
||||
title="Remove machine"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{machines.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<ServerIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">No machines discovered yet. Click "Discover Machines" to scan your network.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Deployment Configuration */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||||
<Cog6ToothIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
||||
Deployment Configuration
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.autoStart}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, autoStart: e.target.checked }))}
|
||||
className="mr-2"
|
||||
/>
|
||||
Auto-start services after deployment
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Health Check Interval (seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={config.healthCheckInterval}
|
||||
onChange={(e) => setConfig(prev => ({ ...prev, healthCheckInterval: parseInt(e.target.value) }))}
|
||||
min="10"
|
||||
max="300"
|
||||
className="input-field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logs Modal */}
|
||||
{showLogs && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-96 overflow-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">Deployment Logs - {machines.find(m => m.id === showLogs)?.hostname}</h3>
|
||||
<button onClick={() => setShowLogs(null)} className="text-gray-400 hover:text-gray-600">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm max-h-64 overflow-y-auto">
|
||||
{deploymentLogs[showLogs]?.map((log, index) => (
|
||||
<div key={index}>{log}</div>
|
||||
)) || <div>No logs available</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Virtual Console Modal */}
|
||||
{showConsole && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 rounded-lg overflow-hidden max-w-4xl w-full max-h-[80vh] flex flex-col">
|
||||
<div className="bg-gray-800 px-4 py-3 flex justify-between items-center border-b border-gray-700">
|
||||
<div className="flex items-center">
|
||||
<ComputerDesktopIcon className="h-5 w-5 text-green-400 mr-2" />
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
SSH Console - {machines.find(m => m.id === showConsole)?.hostname}
|
||||
</h3>
|
||||
<span className="ml-2 text-sm text-gray-400">
|
||||
({machines.find(m => m.id === showConsole)?.ip})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowConsole(null)}
|
||||
className="text-gray-400 hover:text-white ml-4"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 p-4 font-mono text-sm overflow-y-auto bg-gray-900">
|
||||
<div className="text-green-400 space-y-1">
|
||||
{consoleLogs[showConsole]?.length > 0 ? (
|
||||
consoleLogs[showConsole].map((log, index) => (
|
||||
<div key={index} className="whitespace-pre-wrap">{log}</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-500">Waiting for deployment to start...</div>
|
||||
)}
|
||||
{/* Blinking cursor */}
|
||||
<div className="inline-block w-2 h-4 bg-green-400 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 px-4 py-2 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-400">
|
||||
💡 This console shows real-time deployment progress and SSH operations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between pt-6 border-t border-gray-200">
|
||||
<div>
|
||||
{onBack && (
|
||||
|
||||
Reference in New Issue
Block a user