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
*/
public static function generateTestData(int $count = 63): array
public static function generateTestData(int $count = 5): array
{
$testData = [];
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);
$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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -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']);