diff --git a/examples/ServerManager/Services/HetznerService.php b/examples/ServerManager/Services/HetznerService.php index a1c08f9..48dc2c7 100644 --- a/examples/ServerManager/Services/HetznerService.php +++ b/examples/ServerManager/Services/HetznerService.php @@ -48,7 +48,7 @@ class HetznerService /** * Generate test server data for development */ - public static function generateTestData(int $count = 63): array + public static function generateTestData(int $count = 5): array { $testData = []; for ($i = 1; $i <= $count; $i++) { diff --git a/examples/ServerManager/UI/LoadingIndicator.php b/examples/ServerManager/UI/LoadingIndicator.php index 5e43291..e63cdda 100644 --- a/examples/ServerManager/UI/LoadingIndicator.php +++ b/examples/ServerManager/UI/LoadingIndicator.php @@ -17,8 +17,8 @@ class LoadingIndicator extends Container { parent::__construct('flex flex-row items-center gap-1 px-2 ' . $style); - $this->icon = new Icon(IconName::sync, 14, 'text-blue-600'); - $this->label = new Label('Laden...', 'text-xs text-gray-700'); + $this->icon = new Icon(IconName::sync, 16, 'py-2 text-blue-600'); + $this->label = new Label('Laden...', 'py-2 text-gray-700'); $this->addComponent($this->icon); $this->addComponent($this->label); @@ -41,4 +41,3 @@ class LoadingIndicator extends Container return $this->loading; } } - diff --git a/examples/ServerManager/UI/ServerListTab.php b/examples/ServerManager/UI/ServerListTab.php index d55222c..c00af74 100644 --- a/examples/ServerManager/UI/ServerListTab.php +++ b/examples/ServerManager/UI/ServerListTab.php @@ -5,8 +5,8 @@ namespace ServerManager\UI; use PHPNative\Async\TaskManager; use PHPNative\Tailwind\Data\Icon as IconName; use PHPNative\Ui\Widget\Button; -use PHPNative\Ui\Widget\Container; use PHPNative\Ui\Widget\Checkbox; +use PHPNative\Ui\Widget\Container; use PHPNative\Ui\Widget\Icon; use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Widget\TabContainer; @@ -104,7 +104,7 @@ class ServerListTab 'width' => 40, 'render' => [$this, 'renderSelectCell'], ], - ['key' => 'id', 'title' => 'ID', 'width' => 80], + ['key' => 'id', 'title' => 'ID', 'width' => 100], ['key' => 'name', 'title' => 'Name'], ['key' => 'status', 'title' => 'Status', 'width' => 90], ['key' => 'type', 'title' => 'Typ', 'width' => 80], @@ -234,8 +234,7 @@ class ServerListTab string &$currentPrivateKeyPath, Button $rebootButton, Button $updateButton, - ): void - { + ): void { // Table row selection $serverListTab = $this; $this->table->setOnRowSelect(function ($index, $row) use ($serverListTab) { @@ -260,7 +259,7 @@ class ServerListTab $searchTerm = strtolower(trim($value)); if (empty($searchTerm)) { - $serverListTab->table->setData($serverListTab->currentServerData); + $serverListTab->table->setData($serverListTab->currentServerData, true); } else { $filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) { return ( @@ -270,7 +269,7 @@ class ServerListTab })) ); }); - $serverListTab->table->setData(array_values($filteredData)); + $serverListTab->table->setData(array_values($filteredData), false); } }); @@ -325,7 +324,7 @@ class ServerListTab 'os_version' => 'unbekannt', ], $row), $result['servers']); - $serverListTab->table->setData($serverListTab->currentServerData); + $serverListTab->table->setData($serverListTab->currentServerData, false); $serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); // Danach: pro Server asynchron Docker-Infos und Systemstatus nachladen @@ -455,7 +454,7 @@ class ServerListTab $serverListTab->currentServerData[$i]['domains'] = $domains; } $serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker']; - $serverListTab->table->setData($serverListTab->currentServerData); + $serverListTab->table->setData($serverListTab->currentServerData, true); } // Map system status into human-readable table fields @@ -639,22 +638,23 @@ class ServerListTab ]; } - // 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', - ); + // Run update & upgrade non-interactively and then output reboot + update status in one go + $statusOutput = trim($ssh->exec('apt-get update >/dev/null 2>&1 && ' . + 'DEBIAN_FRONTEND=noninteractive apt-get -y upgrade >/dev/null 2>&1; ' . + 'reboot_flag=$([ -f /var/run/reboot-required ] && echo yes || echo no); ' . + 'updates=$(apt-get -s upgrade 2>/dev/null | grep -c "^Inst " || echo 0); ' . + 'echo "$reboot_flag $updates"')); - // 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; + $parts = preg_split('/\s+/', $statusOutput); + $rebootFlag = $parts[0] ?? 'no'; + $updatesCount = isset($parts[1]) && is_numeric($parts[1]) ? ((int) $parts[1]) : 0; + $needsReboot = $rebootFlag === 'yes'; return [ 'index' => $index, 'success' => true, 'updates' => $updatesCount, + 'needs_reboot' => $needsReboot, ]; } catch (\Throwable $e) { return [ @@ -674,11 +674,17 @@ class ServerListTab 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->currentServerData[$index]['updates_available'] = $updates > 0 + ? ('ja (' . $updates . ')') + : 'nein'; } - $serverListTab->table->setData($serverListTab->currentServerData); + if (array_key_exists('needs_reboot', $result)) { + $needsReboot = (bool) $result['needs_reboot']; + $serverListTab->currentServerData[$index]['needs_reboot'] = $needsReboot ? 'ja' : 'nein'; + } + + $serverListTab->table->setData($serverListTab->currentServerData, true); } if ($result['success']) { @@ -778,12 +784,19 @@ class ServerListTab $checkbox = new Checkbox('', $isChecked); $serverListTab = $this; - $checkbox->setOnChange(function (bool $checked) use ($serverListTab, $rowIndex) { - if (!isset($serverListTab->currentServerData[$rowIndex])) { + $rowId = $rowData['id'] ?? null; + $checkbox->setOnChange(function (bool $checked) use (&$serverListTab, &$rowData, $rowId) { + if ($rowId === null) { return; } - $serverListTab->currentServerData[$rowIndex]['selected'] = $checked; + foreach ($serverListTab->currentServerData as $index => $row) { + if (($row['id'] ?? null) === $rowId) { + $serverListTab->currentServerData[$index]['selected'] = $checked; + $serverListTab->table->setData($serverListTab->currentServerData, true); + break; + } + } }); return $checkbox; diff --git a/src/Ui/Widget/Checkbox.php b/src/Ui/Widget/Checkbox.php index 68adc00..9552f93 100644 --- a/src/Ui/Widget/Checkbox.php +++ b/src/Ui/Widget/Checkbox.php @@ -12,12 +12,8 @@ class Checkbox extends Component private $onChange = null; private string $labelText = ''; - public function __construct( - string $label = '', - bool $checked = false, - string $style = '', - $onChange = null, - ) { + public function __construct(string $label = '', bool $checked = false, string $style = '', $onChange = null) + { parent::__construct($style); $this->checked = $checked; @@ -59,12 +55,12 @@ class Checkbox extends Component public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool { // Check if click is within checkbox bounds (not label) - $checkboxSize = 20; + $checkboxSize = 20 * $this->viewport->uiScale; if ( $mouseX >= $this->viewport->x && - $mouseX <= ($this->viewport->x + $checkboxSize) && - $mouseY >= $this->viewport->y && - $mouseY <= ($this->viewport->y + $checkboxSize) + $mouseX <= ($this->viewport->x + $checkboxSize) && + $mouseY >= $this->viewport->y && + $mouseY <= ($this->viewport->y + $checkboxSize) ) { $this->checked = !$this->checked; @@ -80,7 +76,7 @@ class Checkbox extends Component public function render(&$renderer, null|TextRenderer $textRenderer = null): void { - $checkboxSize = 20; + $checkboxSize = 20 * $this->viewport->uiScale; // Draw checkbox border sdl_set_render_draw_color($renderer, 156, 163, 175, 255); // Gray-400 diff --git a/src/Ui/Widget/Table.php b/src/Ui/Widget/Table.php index cb41d1b..30affa9 100644 --- a/src/Ui/Widget/Table.php +++ b/src/Ui/Widget/Table.php @@ -68,15 +68,26 @@ class Table extends Container * Set table data * * @param array $data Array of row data (associative arrays) + * @param bool $preserveScroll Whether to preserve scroll position */ - public function setData(array $data): void + public function setData(array $data, bool $preserveScroll = false): void { $this->rows = $data; + + $scrollPosition = null; + if ($preserveScroll) { + $scrollPosition = $this->bodyContainer->getScrollPosition(); + } + $this->bodyContainer->clearChildren(); foreach ($data as $rowIndex => $row) { $this->addRow($row, $rowIndex); } + + if ($preserveScroll && $scrollPosition !== null) { + $this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']); + } } /** @@ -197,8 +208,8 @@ class Table extends Container return $this->sortAscending ? $result : -$result; }); - // Re-render with sorted data - $this->setData($sortedRows); + // Re-render with sorted data (reset scroll) + $this->setData($sortedRows, false); } /** @@ -217,8 +228,8 @@ class Table extends Container ($this->onRowSelect)($rowIndex, $this->rows[$rowIndex] ?? null); } - // Re-render rows to update selection - $this->setData($this->rows); + // Re-render rows to update selection but keep scroll + $this->setData($this->rows, true); // Restore scroll position $this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']);