This commit is contained in:
Thomas Peterson 2025-11-17 21:31:42 +01:00
parent 7ea3d6cf11
commit 77c9c733ae
5 changed files with 63 additions and 44 deletions

View File

@ -48,7 +48,7 @@ class HetznerService
/** /**
* Generate test server data for development * Generate test server data for development
*/ */
public static function generateTestData(int $count = 63): array public static function generateTestData(int $count = 5): array
{ {
$testData = []; $testData = [];
for ($i = 1; $i <= $count; $i++) { for ($i = 1; $i <= $count; $i++) {

View File

@ -17,8 +17,8 @@ class LoadingIndicator extends Container
{ {
parent::__construct('flex flex-row items-center gap-1 px-2 ' . $style); parent::__construct('flex flex-row items-center gap-1 px-2 ' . $style);
$this->icon = new Icon(IconName::sync, 14, 'text-blue-600'); $this->icon = new Icon(IconName::sync, 16, 'py-2 text-blue-600');
$this->label = new Label('Laden...', 'text-xs text-gray-700'); $this->label = new Label('Laden...', 'py-2 text-gray-700');
$this->addComponent($this->icon); $this->addComponent($this->icon);
$this->addComponent($this->label); $this->addComponent($this->label);
@ -41,4 +41,3 @@ class LoadingIndicator extends Container
return $this->loading; return $this->loading;
} }
} }

View File

@ -5,8 +5,8 @@ namespace ServerManager\UI;
use PHPNative\Async\TaskManager; 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\Checkbox; use PHPNative\Ui\Widget\Checkbox;
use PHPNative\Ui\Widget\Container;
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;
@ -104,7 +104,7 @@ class ServerListTab
'width' => 40, 'width' => 40,
'render' => [$this, 'renderSelectCell'], 'render' => [$this, 'renderSelectCell'],
], ],
['key' => 'id', 'title' => 'ID', 'width' => 80], ['key' => 'id', 'title' => 'ID', 'width' => 100],
['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],
@ -234,8 +234,7 @@ class ServerListTab
string &$currentPrivateKeyPath, string &$currentPrivateKeyPath,
Button $rebootButton, Button $rebootButton,
Button $updateButton, Button $updateButton,
): void ): void {
{
// Table row selection // Table row selection
$serverListTab = $this; $serverListTab = $this;
$this->table->setOnRowSelect(function ($index, $row) use ($serverListTab) { $this->table->setOnRowSelect(function ($index, $row) use ($serverListTab) {
@ -260,7 +259,7 @@ class ServerListTab
$searchTerm = strtolower(trim($value)); $searchTerm = strtolower(trim($value));
if (empty($searchTerm)) { if (empty($searchTerm)) {
$serverListTab->table->setData($serverListTab->currentServerData); $serverListTab->table->setData($serverListTab->currentServerData, true);
} else { } else {
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) { $filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) {
return ( 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', 'os_version' => 'unbekannt',
], $row), $result['servers']); ], $row), $result['servers']);
$serverListTab->table->setData($serverListTab->currentServerData); $serverListTab->table->setData($serverListTab->currentServerData, false);
$serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); $serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
// Danach: pro Server asynchron Docker-Infos und Systemstatus nachladen // Danach: pro Server asynchron Docker-Infos und Systemstatus nachladen
@ -455,7 +454,7 @@ class ServerListTab
$serverListTab->currentServerData[$i]['domains'] = $domains; $serverListTab->currentServerData[$i]['domains'] = $domains;
} }
$serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker']; $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 // Map system status into human-readable table fields
@ -639,22 +638,23 @@ class ServerListTab
]; ];
} }
// Run update & upgrade non-interactively // Run update & upgrade non-interactively and then output reboot + update status in one go
$ssh->exec( $statusOutput = trim($ssh->exec('apt-get update >/dev/null 2>&1 && ' .
'apt-get update >/dev/null 2>&1 && ' . 'DEBIAN_FRONTEND=noninteractive apt-get -y upgrade >/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 $parts = preg_split('/\s+/', $statusOutput);
$updatesOutput = trim($ssh->exec( $rebootFlag = $parts[0] ?? 'no';
'apt-get -s upgrade 2>/dev/null | grep -c "^Inst " || echo 0', $updatesCount = isset($parts[1]) && is_numeric($parts[1]) ? ((int) $parts[1]) : 0;
)); $needsReboot = $rebootFlag === 'yes';
$updatesCount = is_numeric($updatesOutput) ? (int) $updatesOutput : 0;
return [ return [
'index' => $index, 'index' => $index,
'success' => true, 'success' => true,
'updates' => $updatesCount, 'updates' => $updatesCount,
'needs_reboot' => $needsReboot,
]; ];
} catch (\Throwable $e) { } catch (\Throwable $e) {
return [ return [
@ -674,11 +674,17 @@ class ServerListTab
if ($index !== null && isset($serverListTab->currentServerData[$index])) { if ($index !== null && isset($serverListTab->currentServerData[$index])) {
if (isset($result['updates'])) { if (isset($result['updates'])) {
$updates = (int) $result['updates']; $updates = (int) $result['updates'];
$serverListTab->currentServerData[$index]['updates_available'] = $serverListTab->currentServerData[$index]['updates_available'] = $updates > 0
$updates > 0 ? ('ja (' . $updates . ')') : 'nein'; ? ('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']) { if ($result['success']) {
@ -778,12 +784,19 @@ class ServerListTab
$checkbox = new Checkbox('', $isChecked); $checkbox = new Checkbox('', $isChecked);
$serverListTab = $this; $serverListTab = $this;
$checkbox->setOnChange(function (bool $checked) use ($serverListTab, $rowIndex) { $rowId = $rowData['id'] ?? null;
if (!isset($serverListTab->currentServerData[$rowIndex])) { $checkbox->setOnChange(function (bool $checked) use (&$serverListTab, &$rowData, $rowId) {
if ($rowId === null) {
return; 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; return $checkbox;

View File

@ -12,12 +12,8 @@ class Checkbox extends Component
private $onChange = null; private $onChange = null;
private string $labelText = ''; private string $labelText = '';
public function __construct( public function __construct(string $label = '', bool $checked = false, string $style = '', $onChange = null)
string $label = '', {
bool $checked = false,
string $style = '',
$onChange = null,
) {
parent::__construct($style); parent::__construct($style);
$this->checked = $checked; $this->checked = $checked;
@ -59,7 +55,7 @@ class Checkbox extends Component
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)
$checkboxSize = 20; $checkboxSize = 20 * $this->viewport->uiScale;
if ( if (
$mouseX >= $this->viewport->x && $mouseX >= $this->viewport->x &&
$mouseX <= ($this->viewport->x + $checkboxSize) && $mouseX <= ($this->viewport->x + $checkboxSize) &&
@ -80,7 +76,7 @@ class Checkbox extends Component
public function render(&$renderer, null|TextRenderer $textRenderer = null): void public function render(&$renderer, null|TextRenderer $textRenderer = null): void
{ {
$checkboxSize = 20; $checkboxSize = 20 * $this->viewport->uiScale;
// Draw checkbox border // Draw checkbox border
sdl_set_render_draw_color($renderer, 156, 163, 175, 255); // Gray-400 sdl_set_render_draw_color($renderer, 156, 163, 175, 255); // Gray-400

View File

@ -68,15 +68,26 @@ class Table extends Container
* Set table data * Set table data
* *
* @param array $data Array of row data (associative arrays) * @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; $this->rows = $data;
$scrollPosition = null;
if ($preserveScroll) {
$scrollPosition = $this->bodyContainer->getScrollPosition();
}
$this->bodyContainer->clearChildren(); $this->bodyContainer->clearChildren();
foreach ($data as $rowIndex => $row) { foreach ($data as $rowIndex => $row) {
$this->addRow($row, $rowIndex); $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; return $this->sortAscending ? $result : -$result;
}); });
// Re-render with sorted data // Re-render with sorted data (reset scroll)
$this->setData($sortedRows); $this->setData($sortedRows, false);
} }
/** /**
@ -217,8 +228,8 @@ class Table extends Container
($this->onRowSelect)($rowIndex, $this->rows[$rowIndex] ?? null); ($this->onRowSelect)($rowIndex, $this->rows[$rowIndex] ?? null);
} }
// Re-render rows to update selection // Re-render rows to update selection but keep scroll
$this->setData($this->rows); $this->setData($this->rows, true);
// Restore scroll position // Restore scroll position
$this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']); $this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']);