This commit is contained in:
Thomas Peterson 2025-11-17 13:28:24 +01:00
parent 21bc637ced
commit 7ea3d6cf11
2 changed files with 228 additions and 3 deletions

View File

@ -6,6 +6,7 @@ use PHPNative\Async\TaskManager;
use PHPNative\Tailwind\Data\Icon as IconName; use PHPNative\Tailwind\Data\Icon as IconName;
use PHPNative\Ui\Widget\Button; use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container; use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Checkbox;
use PHPNative\Ui\Widget\Icon; use PHPNative\Ui\Widget\Icon;
use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\TabContainer; use PHPNative\Ui\Widget\TabContainer;
@ -65,6 +66,22 @@ class ServerListTab
$this->refreshButton->setIcon($refreshIcon); $this->refreshButton->setIcon($refreshIcon);
$headerRow->addComponent($this->refreshButton); $headerRow->addComponent($this->refreshButton);
// Bulk action buttons for selected servers
$rebootButton = new Button(
'Reboot ausgewählte',
'px-3 py-2 bg-red-600 rounded hover:bg-red-700',
null,
'text-white',
);
$updateButton = new Button(
'Update ausgewählte',
'px-3 py-2 bg-amber-600 rounded hover:bg-amber-700',
null,
'text-white',
);
$headerRow->addComponent($rebootButton);
$headerRow->addComponent($updateButton);
// Loading indicator (top-right in the server tab header) // Loading indicator (top-right in the server tab header)
$this->loadingIndicator = new LoadingIndicator('ml-auto'); $this->loadingIndicator = new LoadingIndicator('ml-auto');
$headerRow->addComponent($this->loadingIndicator); $headerRow->addComponent($this->loadingIndicator);
@ -81,7 +98,13 @@ class ServerListTab
// Table // Table
$this->table = new Table(style: ' flex-1'); $this->table = new Table(style: ' flex-1');
$this->table->setColumns([ $this->table->setColumns([
['key' => 'id', 'title' => 'ID', 'width' => 100], [
'key' => 'selected',
'title' => '',
'width' => 40,
'render' => [$this, 'renderSelectCell'],
],
['key' => 'id', 'title' => 'ID', 'width' => 80],
['key' => 'name', 'title' => 'Name'], ['key' => 'name', 'title' => 'Name'],
['key' => 'status', 'title' => 'Status', 'width' => 90], ['key' => 'status', 'title' => 'Status', 'width' => 90],
['key' => 'type', 'title' => 'Typ', 'width' => 80], ['key' => 'type', 'title' => 'Typ', 'width' => 80],
@ -203,10 +226,15 @@ class ServerListTab
$this->tab->addComponent($detailPanel); $this->tab->addComponent($detailPanel);
// Setup event handlers // Setup event handlers
$this->setupEventHandlers($currentApiKey, $currentPrivateKeyPath); $this->setupEventHandlers($currentApiKey, $currentPrivateKeyPath, $rebootButton, $updateButton);
} }
private function setupEventHandlers(string &$currentApiKey, string &$currentPrivateKeyPath): void private function setupEventHandlers(
string &$currentApiKey,
string &$currentPrivateKeyPath,
Button $rebootButton,
Button $updateButton,
): void
{ {
// Table row selection // Table row selection
$serverListTab = $this; $serverListTab = $this;
@ -493,6 +521,174 @@ class ServerListTab
$serverListTab->loadingIndicator->setLoading(false); $serverListTab->loadingIndicator->setLoading(false);
}); });
}); });
// Reboot selected servers
$rebootButton->setOnClick(function () use (&$currentPrivateKeyPath, $serverListTab) {
$selected = [];
foreach ($serverListTab->currentServerData as $index => $row) {
if (!empty($row['selected'])) {
$selected[] = ['index' => $index, 'row' => $row];
}
}
if (empty($selected)) {
$serverListTab->statusLabel->setText('Keine Server ausgewählt für Reboot');
return;
}
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
$serverListTab->statusLabel->setText('Private Key Pfad nicht konfiguriert');
return;
}
foreach ($selected as $item) {
$row = $item['row'];
$ip = $row['ipv4'] ?? '';
$name = $row['name'] ?? $ip;
$index = $item['index'];
if (empty($ip)) {
continue;
}
$task = TaskManager::getInstance()->runAsync(function () use ($ip, $currentPrivateKeyPath, $index) {
try {
$ssh = new \phpseclib3\Net\SSH2($ip);
$key = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey(file_get_contents(
$currentPrivateKeyPath,
));
if (!$ssh->login('root', $key)) {
return [
'index' => $index,
'success' => false,
'error' => 'SSH Login fehlgeschlagen',
];
}
$ssh->exec('reboot');
return [
'index' => $index,
'success' => true,
];
} catch (\Throwable $e) {
return [
'index' => $index,
'success' => false,
'error' => 'SSH-Fehler: ' . $e->getMessage(),
];
}
});
$task->onComplete(function ($result) use (&$serverListTab, $name) {
if (!is_array($result) || !array_key_exists('success', $result)) {
return;
}
if ($result['success']) {
$serverListTab->statusLabel->setText('Reboot ausgelöst für ' . $name);
} elseif (isset($result['error'])) {
$serverListTab->statusLabel->setText('Reboot Fehler bei ' . $name . ': ' . $result['error']);
}
});
}
});
// Update selected servers
$updateButton->setOnClick(function () use (&$currentPrivateKeyPath, $serverListTab) {
$selected = [];
foreach ($serverListTab->currentServerData as $index => $row) {
if (!empty($row['selected'])) {
$selected[] = ['index' => $index, 'row' => $row];
}
}
if (empty($selected)) {
$serverListTab->statusLabel->setText('Keine Server ausgewählt für Updates');
return;
}
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
$serverListTab->statusLabel->setText('Private Key Pfad nicht konfiguriert');
return;
}
foreach ($selected as $item) {
$row = $item['row'];
$ip = $row['ipv4'] ?? '';
$name = $row['name'] ?? $ip;
$index = $item['index'];
if (empty($ip)) {
continue;
}
$task = TaskManager::getInstance()->runAsync(function () use ($ip, $currentPrivateKeyPath, $index) {
try {
$ssh = new \phpseclib3\Net\SSH2($ip);
$key = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey(file_get_contents(
$currentPrivateKeyPath,
));
if (!$ssh->login('root', $key)) {
return [
'index' => $index,
'success' => false,
'error' => 'SSH Login fehlgeschlagen',
];
}
// Run update & upgrade non-interactively
$ssh->exec(
'apt-get update >/dev/null 2>&1 && ' .
'DEBIAN_FRONTEND=noninteractive apt-get -y upgrade >/dev/null 2>&1',
);
// Re-check number of available updates
$updatesOutput = trim($ssh->exec(
'apt-get -s upgrade 2>/dev/null | grep -c "^Inst " || echo 0',
));
$updatesCount = is_numeric($updatesOutput) ? (int) $updatesOutput : 0;
return [
'index' => $index,
'success' => true,
'updates' => $updatesCount,
];
} catch (\Throwable $e) {
return [
'index' => $index,
'success' => false,
'error' => 'SSH-Fehler: ' . $e->getMessage(),
];
}
});
$task->onComplete(function ($result) use (&$serverListTab, $name) {
if (!is_array($result) || !array_key_exists('success', $result)) {
return;
}
$index = $result['index'] ?? null;
if ($index !== null && isset($serverListTab->currentServerData[$index])) {
if (isset($result['updates'])) {
$updates = (int) $result['updates'];
$serverListTab->currentServerData[$index]['updates_available'] =
$updates > 0 ? ('ja (' . $updates . ')') : 'nein';
}
$serverListTab->table->setData($serverListTab->currentServerData);
}
if ($result['success']) {
$serverListTab->statusLabel->setText('Updates ausgeführt für ' . $name);
} elseif (isset($result['error'])) {
$serverListTab->statusLabel->setText('Update Fehler bei ' . $name . ': ' . $result['error']);
}
});
}
});
} }
public function getContainer(): Container public function getContainer(): Container
@ -575,4 +771,21 @@ class ServerListTab
$this->detailDomainsContainer->addComponent($button); $this->detailDomainsContainer->addComponent($button);
} }
} }
public function renderSelectCell(array $rowData, int $rowIndex): Checkbox
{
$isChecked = (bool) ($rowData['selected'] ?? false);
$checkbox = new Checkbox('', $isChecked);
$serverListTab = $this;
$checkbox->setOnChange(function (bool $checked) use ($serverListTab, $rowIndex) {
if (!isset($serverListTab->currentServerData[$rowIndex])) {
return;
}
$serverListTab->currentServerData[$rowIndex]['selected'] = $checked;
});
return $checkbox;
}
} }

View File

@ -44,6 +44,18 @@ class Checkbox extends Component
$this->onChange = $onChange; $this->onChange = $onChange;
} }
public function layout(null|TextRenderer $textRenderer = null): void
{
parent::layout($textRenderer);
// Force a compact, fixed size for the checkbox
$checkboxSize = 20;
$this->viewport->width = $checkboxSize;
$this->viewport->height = $checkboxSize;
$this->contentViewport->width = $checkboxSize;
$this->contentViewport->height = $checkboxSize;
}
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{ {
// Check if click is within checkbox bounds (not label) // Check if click is within checkbox bounds (not label)