feat: VM/CT Start/Stop im Frontend + Agent proxmox_action Handler
- Agent: neuer 'proxmox_action' Command (start/stop/shutdown fuer VMs und LXC)
- Backend: POST /api/v1/agents/{id}/proxmox/action Endpoint
- Frontend: Start/Stop/Shutdown Buttons in VMs- und Container-Tab
- Play (gruen) = starten (nur wenn gestoppt)
- Power (gelb) = Graceful Shutdown (nur VMs, nur wenn running)
- Stop (rot) = Hard Stop (wenn running)
- api/client.js: proxmoxAction() Methode
This commit is contained in:
@@ -224,6 +224,10 @@ class ApiClient {
|
||||
return this.post(`/api/v1/agents/${agentId}/exec`, { command, timeout })
|
||||
}
|
||||
|
||||
proxmoxAction(agentId, type, vmid, action) {
|
||||
return this.post(`/api/v1/agents/${agentId}/proxmox/action`, { type, vmid, action })
|
||||
}
|
||||
|
||||
// Users
|
||||
getUsers() {
|
||||
return this.get('/api/v1/users')
|
||||
|
||||
@@ -117,9 +117,9 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
|
||||
) : tab === 'overview' ? (
|
||||
<OverviewTab sys={sys} proxmox={proxmox} />
|
||||
) : tab === 'vms' ? (
|
||||
<VmsTab proxmox={proxmox} />
|
||||
<VmsTab proxmox={proxmox} agentId={agentId} />
|
||||
) : tab === 'containers' ? (
|
||||
<ContainersTab proxmox={proxmox} />
|
||||
<ContainersTab proxmox={proxmox} agentId={agentId} />
|
||||
) : tab === 'storage' ? (
|
||||
<StorageTab proxmox={proxmox} />
|
||||
) : tab === 'zfs' ? (
|
||||
@@ -617,16 +617,76 @@ function OverviewTab({ sys, proxmox }) {
|
||||
)
|
||||
}
|
||||
|
||||
function VmsTab({ proxmox }) {
|
||||
function VmActionButtons({ status, loading, onStart, onStop, onShutdown }) {
|
||||
const isRunning = status === 'running'
|
||||
const isStopped = status === 'stopped'
|
||||
|
||||
if (loading) {
|
||||
return <span className="text-xs text-gray-500 animate-pulse">...</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{isStopped && (
|
||||
<button
|
||||
onClick={onStart}
|
||||
title="Starten"
|
||||
className="p-1 rounded text-green-400 hover:bg-green-900/40 hover:text-green-300 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M6.3 2.84A1.5 1.5 0 004 4.11v11.78a1.5 1.5 0 002.3 1.27l9.344-5.891a1.5 1.5 0 000-2.538L6.3 2.84z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{isRunning && onShutdown && (
|
||||
<button
|
||||
onClick={onShutdown}
|
||||
title="Graceful Shutdown"
|
||||
className="p-1 rounded text-yellow-400 hover:bg-yellow-900/40 hover:text-yellow-300 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{isRunning && (
|
||||
<button
|
||||
onClick={onStop}
|
||||
title="Hard Stop"
|
||||
className="p-1 rounded text-red-400 hover:bg-red-900/40 hover:text-red-300 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4 4h12v12H4V4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VmsTab({ proxmox, agentId }) {
|
||||
const vms = proxmox.vms || []
|
||||
|
||||
// Sort: running first, then by name
|
||||
const [pending, setPending] = useState({}) // vmid -> true wenn Action läuft
|
||||
const [errors, setErrors] = useState({})
|
||||
|
||||
const sortedVms = [...vms].sort((a, b) => {
|
||||
if (a.status === 'running' && b.status !== 'running') return -1
|
||||
if (a.status !== 'running' && b.status === 'running') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
const doAction = async (vmid, action) => {
|
||||
setPending(p => ({ ...p, [vmid]: true }))
|
||||
setErrors(e => ({ ...e, [vmid]: null }))
|
||||
try {
|
||||
await api.proxmoxAction(agentId, 'vm', vmid, action)
|
||||
} catch (err) {
|
||||
setErrors(e => ({ ...e, [vmid]: err.message }))
|
||||
} finally {
|
||||
setPending(p => ({ ...p, [vmid]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-sm font-medium text-gray-300">Virtuelle Maschinen ({vms.length})</h3>
|
||||
@@ -644,6 +704,7 @@ function VmsTab({ proxmox }) {
|
||||
<th className="px-3 py-2">RAM</th>
|
||||
<th className="px-3 py-2">Disk</th>
|
||||
<th className="px-3 py-2">Uptime</th>
|
||||
<th className="px-3 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
@@ -653,21 +714,31 @@ function VmsTab({ proxmox }) {
|
||||
<td className="px-3 py-2 text-white font-medium">{vm.name}</td>
|
||||
<td className="px-3 py-2">
|
||||
<StatusBadge status={vm.status} />
|
||||
{errors[vm.vmid] && <div className="text-red-400 text-xs mt-1">{errors[vm.vmid]}</div>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{vm.cpu_usage != null ? `${vm.cpu_usage.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{vm.memory_used && vm.memory_max ?
|
||||
{vm.memory_used && vm.memory_max ?
|
||||
`${formatBytes(vm.memory_used)} / ${formatBytes(vm.memory_max)}` : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{vm.disk_used != null && vm.disk_max ?
|
||||
{vm.disk_used != null && vm.disk_max ?
|
||||
`${formatBytes(vm.disk_used)} / ${formatBytes(vm.disk_max)}` : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{vm.uptime ? formatUptime(vm.uptime) : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<VmActionButtons
|
||||
status={vm.status}
|
||||
loading={!!pending[vm.vmid]}
|
||||
onStart={() => doAction(vm.vmid, 'start')}
|
||||
onStop={() => doAction(vm.vmid, 'stop')}
|
||||
onShutdown={() => doAction(vm.vmid, 'shutdown')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -678,16 +749,29 @@ function VmsTab({ proxmox }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ContainersTab({ proxmox }) {
|
||||
function ContainersTab({ proxmox, agentId }) {
|
||||
const containers = proxmox.containers || []
|
||||
|
||||
// Sort: running first, then by name
|
||||
const [pending, setPending] = useState({})
|
||||
const [errors, setErrors] = useState({})
|
||||
|
||||
const sortedCt = [...containers].sort((a, b) => {
|
||||
if (a.status === 'running' && b.status !== 'running') return -1
|
||||
if (a.status !== 'running' && b.status === 'running') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
const doAction = async (vmid, action) => {
|
||||
setPending(p => ({ ...p, [vmid]: true }))
|
||||
setErrors(e => ({ ...e, [vmid]: null }))
|
||||
try {
|
||||
await api.proxmoxAction(agentId, 'ct', vmid, action)
|
||||
} catch (err) {
|
||||
setErrors(e => ({ ...e, [vmid]: err.message }))
|
||||
} finally {
|
||||
setPending(p => ({ ...p, [vmid]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-sm font-medium text-gray-300">Container ({containers.length})</h3>
|
||||
@@ -704,6 +788,7 @@ function ContainersTab({ proxmox }) {
|
||||
<th className="px-3 py-2">CPU</th>
|
||||
<th className="px-3 py-2">RAM</th>
|
||||
<th className="px-3 py-2">Uptime</th>
|
||||
<th className="px-3 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
@@ -713,17 +798,26 @@ function ContainersTab({ proxmox }) {
|
||||
<td className="px-3 py-2 text-white font-medium">{ct.name}</td>
|
||||
<td className="px-3 py-2">
|
||||
<StatusBadge status={ct.status} />
|
||||
{errors[ct.vmid] && <div className="text-red-400 text-xs mt-1">{errors[ct.vmid]}</div>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{ct.cpu_usage != null ? `${ct.cpu_usage.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{ct.memory_used && ct.memory_max ?
|
||||
{ct.memory_used && ct.memory_max ?
|
||||
`${formatBytes(ct.memory_used)} / ${formatBytes(ct.memory_max)}` : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
{ct.uptime ? formatUptime(ct.uptime) : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<VmActionButtons
|
||||
status={ct.status}
|
||||
loading={!!pending[ct.vmid]}
|
||||
onStart={() => doAction(ct.vmid, 'start')}
|
||||
onStop={() => doAction(ct.vmid, 'stop')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user