sdl3/examples/ServerManager/UI/ServerListTab.php
2025-11-17 21:31:42 +01:00

805 lines
35 KiB
PHP

<?php
namespace ServerManager\UI;
use PHPNative\Async\TaskManager;
use PHPNative\Tailwind\Data\Icon as IconName;
use PHPNative\Ui\Widget\Button;
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;
use PHPNative\Ui\Widget\Table;
use PHPNative\Ui\Widget\TextInput;
use ServerManager\Services\HetznerService;
use ServerManager\UI\LoadingIndicator;
class ServerListTab
{
private Container $tab;
private Table $table;
private TextInput $searchInput;
private Label $statusLabel;
private Button $refreshButton;
private Button $sftpButton;
private Button $sshTerminalButton;
public array $currentServerData = [];
public null|array $selectedServer = null;
private Label $detailId;
private Label $detailName;
private Label $detailStatus;
private Label $detailType;
private Label $detailIpv4;
private Container $detailDomainsContainer;
private LoadingIndicator $loadingIndicator;
public function __construct(
string &$apiKey,
string &$privateKeyPath,
TabContainer $tabContainer,
Label $statusLabel,
) {
$this->statusLabel = $statusLabel;
$currentApiKey = &$apiKey;
$currentPrivateKeyPath = &$privateKeyPath;
// Create main tab container
$this->tab = new Container('flex flex-row p-4 gap-4');
// Left side: Table with search and refresh
$leftSide = new Container('flex flex-col gap-2 flex-1');
// Header row with refresh button and loading indicator on the right
$headerRow = new Container('flex flex-row items-center gap-2');
// Refresh button
$this->refreshButton = new Button(
'Server aktualisieren',
'flex shadow-lg/50 flex-row gap-2 px-4 py-2 bg-blue-600 rounded hover:bg-blue-700',
null,
'text-white',
);
$refreshIcon = new Icon(IconName::sync, 16, 'text-white');
$this->refreshButton->setIcon($refreshIcon);
$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)
$this->loadingIndicator = new LoadingIndicator('ml-auto');
$headerRow->addComponent($this->loadingIndicator);
$leftSide->addComponent($headerRow);
// Search input
$this->searchInput = new TextInput(
'Suche...',
'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black mb-2',
);
$leftSide->addComponent($this->searchInput);
// Table
$this->table = new Table(style: ' flex-1');
$this->table->setColumns([
[
'key' => 'selected',
'title' => '',
'width' => 40,
'render' => [$this, 'renderSelectCell'],
],
['key' => 'id', 'title' => 'ID', 'width' => 100],
['key' => 'name', 'title' => 'Name'],
['key' => 'status', 'title' => 'Status', 'width' => 90],
['key' => 'type', 'title' => 'Typ', 'width' => 80],
['key' => 'ipv4', 'title' => 'IPv4', 'width' => 140],
['key' => 'docker_status', 'title' => 'Docker', 'width' => 80],
['key' => 'os_version', 'title' => 'Ubuntu', 'width' => 180],
[
'key' => 'needs_reboot',
'title' => 'Neustart',
'width' => 110,
'render' => [$this, 'renderNeedsRebootCell'],
],
[
'key' => 'updates_available',
'title' => 'Updates',
'width' => 130,
'render' => [$this, 'renderUpdatesCell'],
],
]);
// Load initial test data
$this->currentServerData = HetznerService::generateTestData();
$this->table->setData($this->currentServerData);
$leftSide->addComponent($this->table);
// Right side: Detail panel
$detailPanel = new Container('flex flex-col gap-3 w-120 bg-white border-2 border-gray-300 rounded p-4');
$detailTitle = new Label('Server Details', 'text-xl font-bold text-black mb-2');
$detailPanel->addComponent($detailTitle);
$this->detailId = new Label('-', 'text-sm text-gray-900 font-mono');
$this->detailName = new Label('-', 'text-lg font-semibold text-black');
$this->detailStatus = new Label('-', 'text-sm text-gray-700');
$this->detailType = new Label('-', 'text-sm text-gray-700');
$this->detailIpv4 = new Label('-', 'text-sm text-gray-700 font-mono');
$detailPanel->addComponent(new Label('ID:', 'text-xs text-gray-500'));
$detailPanel->addComponent($this->detailId);
$detailPanel->addComponent(new Label('Name:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($this->detailName);
$detailPanel->addComponent(new Label('Status:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($this->detailStatus);
$detailPanel->addComponent(new Label('Typ:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($this->detailType);
$detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($this->detailIpv4);
// Domains list
$detailPanel->addComponent(new Label('Domains:', 'text-xs text-gray-500 mt-2'));
$this->detailDomainsContainer = new Container('flex flex-col gap-1');
$detailPanel->addComponent($this->detailDomainsContainer);
// SFTP Manager Button (handler will be set by SftpManagerTab)
$this->sftpButton = new Button(
'SFTP Manager öffnen',
'w-full rounded-md bg-blue-500 px-3 py-2 text-sm font-semibold shadow-lg shadow-blue-500/50 focus:outline-none flex flex-row gap-2',
null,
'text-white',
);
$sftpIcon = new Icon(IconName::folder, 16, 'text-white');
$this->sftpButton->setIcon($sftpIcon);
$detailPanel->addComponent($this->sftpButton);
// SSH Terminal Button
$this->sshTerminalButton = new Button(
'SSH Terminal öffnen',
'w-full border border-gray-300 rounded px-3 py-2 flex shadow-lg/100 shadow-lime-300 flex-row gap-2 bg-lime-300 text-black mb-2',
null,
'text-black',
);
$sshTerminalIcon = new Icon(IconName::terminal, 16, 'text-black');
$this->sshTerminalButton->setIcon($sshTerminalIcon);
$serverListTab = $this;
$this->sshTerminalButton->setOnClick(function () use ($serverListTab, &$currentPrivateKeyPath) {
if ($serverListTab->selectedServer === null) {
$serverListTab->statusLabel->setText('Kein Server ausgewählt');
return;
}
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
$serverListTab->statusLabel->setText('Private Key Pfad nicht konfiguriert');
return;
}
$host = $serverListTab->selectedServer['ipv4'];
$keyPath = escapeshellarg($currentPrivateKeyPath);
$sshCommand = "ssh -i {$keyPath} root@{$host}";
$terminals = [
'gnome-terminal -- ' . $sshCommand,
'konsole -e ' . $sshCommand,
'xterm -e ' . $sshCommand,
'x-terminal-emulator -e ' . $sshCommand,
];
$opened = false;
foreach ($terminals as $terminalCmd) {
exec($terminalCmd . ' > /dev/null 2>&1 &', $output, $returnCode);
if ($returnCode === 0) {
$opened = true;
$serverListTab->statusLabel->setText(
'SSH Terminal geöffnet für ' . $serverListTab->selectedServer['name'],
);
break;
}
}
if (!$opened) {
$serverListTab->statusLabel->setText('Konnte kein Terminal öffnen. SSH Befehl: ' . $sshCommand);
}
});
$detailPanel->addComponent($this->sshTerminalButton);
$this->tab->addComponent($leftSide);
$this->tab->addComponent($detailPanel);
// Setup event handlers
$this->setupEventHandlers($currentApiKey, $currentPrivateKeyPath, $rebootButton, $updateButton);
}
private function setupEventHandlers(
string &$currentApiKey,
string &$currentPrivateKeyPath,
Button $rebootButton,
Button $updateButton,
): void {
// Table row selection
$serverListTab = $this;
$this->table->setOnRowSelect(function ($index, $row) use ($serverListTab) {
if ($row) {
$serverListTab->statusLabel->setText("Server: {$row['name']} - {$row['status']} ({$row['ipv4']})");
$serverListTab->detailId->setText("#{$row['id']}");
$serverListTab->detailName->setText($row['name']);
$serverListTab->detailStatus->setText($row['status']);
$serverListTab->detailType->setText($row['type']);
$serverListTab->detailIpv4->setText($row['ipv4']);
$domains = $row['domains'] ?? [];
$serverListTab->updateDomainDetails(is_array($domains) ? $domains : []);
$serverListTab->selectedServer = $row;
}
});
// Search functionality
$this->searchInput->setOnChange(function ($value) use ($serverListTab) {
$searchTerm = strtolower(trim($value));
if (empty($searchTerm)) {
$serverListTab->table->setData($serverListTab->currentServerData, true);
} else {
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) {
return (
str_contains(strtolower($row['name']), $searchTerm) ||
count(array_filter($row['domains'], function ($item) use ($searchTerm) {
return str_contains(strtolower($item), $searchTerm);
}))
);
});
$serverListTab->table->setData(array_values($filteredData), false);
}
});
// Refresh button - use TaskManager directly so we can control the loading indicator
$this->refreshButton->setOnClick(function () use (&$currentApiKey, $serverListTab, &$currentPrivateKeyPath) {
$serverListTab->loadingIndicator->setLoading(true);
$task = TaskManager::getInstance()->runAsync(function () use (&$currentApiKey) {
try {
if (empty($currentApiKey)) {
return ['error' => 'Kein API-Key konfiguriert'];
}
$hetznerClient = new \LKDev\HetznerCloud\HetznerAPIClient($currentApiKey);
$servers = [];
foreach ($hetznerClient->servers()->all() as $server) {
$servers[] = [
'id' => $server->id,
'name' => $server->name,
'status' => $server->status,
'type' => $server->serverType->name,
'ipv4' => $server->publicNet->ipv4->ip,
'docker_status' => 'pending',
'domains' => [],
];
}
return [
'success' => true,
'servers' => $servers,
'count' => count($servers),
];
} catch (\Exception $e) {
return ['error' => 'Exception: ' . $e->getMessage()];
}
});
$task->onComplete(function ($result) use (&$serverListTab, &$currentPrivateKeyPath) {
if (is_array($result)) {
if (isset($result['error'])) {
$serverListTab->statusLabel->setText('Fehler: ' . $result['error']);
echo "Error: {$result['error']}\n";
} elseif (isset($result['success'], $result['servers'])) {
// Basisdaten setzen, Docker-Status initial auf "pending"
$serverListTab->currentServerData = array_map(static fn($row) => array_merge([
'docker_status' => 'pending',
'docker' => null,
'docker_error' => null,
'needs_reboot' => 'unbekannt',
'updates_available' => 'unbekannt',
'os_version' => 'unbekannt',
], $row), $result['servers']);
$serverListTab->table->setData($serverListTab->currentServerData, false);
$serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
// Danach: pro Server asynchron Docker-Infos und Systemstatus nachladen
foreach ($serverListTab->currentServerData as $index => $row) {
$ip = $row['ipv4'] ?? '';
if (empty($ip) || empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
$serverListTab->currentServerData[$index]['docker_error'] = 'Kein gültiger Private-Key oder IP';
$serverListTab->currentServerData[$index]['docker_status'] = 'error';
continue;
}
$dockerTask = 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,
'docker' => null,
'docker_error' => 'SSH Login fehlgeschlagen',
'docker_status' => 'error',
];
}
// Check if reboot is required (Ubuntu)
$rebootOutput = trim($ssh->exec(
'[ -f /var/run/reboot-required ] && echo yes || echo no',
));
$needsReboot = $rebootOutput === 'yes';
// Check number of available package updates (Ubuntu)
$updatesOutput = trim($ssh->exec(
'apt-get -s upgrade 2>/dev/null | grep -c "^Inst " || echo 0',
));
$updatesCount = is_numeric($updatesOutput) ? ((int) $updatesOutput) : 0;
// Read Ubuntu version (PRETTY_NAME from /etc/os-release)
$osOutput = trim($ssh->exec(
'grep "^PRETTY_NAME=" /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d \'"\'',
));
$osVersion = $osOutput !== '' ? $osOutput : 'unbekannt';
// Docker status for main application container
$output = $ssh->exec('docker inspect psc-web-1');
if (empty($output)) {
return [
'index' => $index,
'docker' => null,
'docker_error' => 'Leere Docker-Antwort',
'docker_status' => 'error',
'needs_reboot' => $needsReboot,
'updates' => $updatesCount,
'os_version' => $osVersion,
];
}
$json = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'index' => $index,
'docker' => null,
'docker_error' => 'Ungültige Docker-JSON-Antwort',
'docker_status' => 'error',
'needs_reboot' => $needsReboot,
'updates' => $updatesCount,
'os_version' => $osVersion,
];
}
return [
'index' => $index,
'docker' => $json,
'docker_error' => null,
'docker_status' => 'ok',
'needs_reboot' => $needsReboot,
'updates' => $updatesCount,
'os_version' => $osVersion,
];
} catch (\Throwable $e) {
return [
'index' => $index,
'docker' => null,
'docker_error' => 'SSH-Fehler: ' . $e->getMessage(),
'docker_status' => 'error',
'needs_reboot' => null,
'updates' => null,
'os_version' => null,
];
}
});
$dockerTask->onComplete(function ($dockerResult) use (&$serverListTab) {
if (!is_array($dockerResult) || !isset($dockerResult['index'])) {
return;
}
$i = $dockerResult['index'];
if (!isset($serverListTab->currentServerData[$i])) {
return;
}
if (array_key_exists('docker', $dockerResult)) {
if (isset($dockerResult['docker'][0]['Config']['Env'])) {
$hosts = array_filter(
$dockerResult['docker'][0]['Config']['Env'],
function ($item) {
if (str_starts_with($item, 'LETSENCRYPT_HOST')) {
return true;
}
return false;
},
);
$domains = explode(
',',
substr(array_first($hosts), strlen('LETSENCRYPT_HOST=')),
);
$serverListTab->currentServerData[$i]['domains'] = $domains;
}
$serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker'];
$serverListTab->table->setData($serverListTab->currentServerData, true);
}
// Map system status into human-readable table fields
if (array_key_exists('needs_reboot', $dockerResult)) {
$needsReboot = $dockerResult['needs_reboot'];
if ($needsReboot === null) {
$serverListTab->currentServerData[$i]['needs_reboot'] = 'unbekannt';
} else {
$serverListTab->currentServerData[$i]['needs_reboot'] = $needsReboot
? 'ja'
: 'nein';
}
}
if (array_key_exists('updates', $dockerResult)) {
$updates = (int) ($dockerResult['updates'] ?? 0);
$serverListTab->currentServerData[$i]['updates_available'] = $updates > 0
? ('ja (' . $updates . ')')
: 'nein';
}
if (array_key_exists('os_version', $dockerResult)) {
$osVersion = $dockerResult['os_version'];
$serverListTab->currentServerData[$i]['os_version'] = $osVersion !== null
? $osVersion
: 'unbekannt';
}
if (array_key_exists('docker_error', $dockerResult)) {
$serverListTab->currentServerData[$i]['docker_error'] =
$dockerResult['docker_error'];
}
if (array_key_exists('docker_status', $dockerResult)) {
$serverListTab->currentServerData[$i]['docker_status'] =
$dockerResult['docker_status'];
}
});
$dockerTask->onError(function ($error) use (&$serverListTab, $index) {
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
? $error->getMessage()
: ((string) $error);
if (isset($serverListTab->currentServerData[$index])) {
$serverListTab->currentServerData[$index]['docker_error'] =
'Async Fehler: ' . $errorMsg;
}
});
}
}
}
$serverListTab->loadingIndicator->setLoading(false);
});
$task->onError(function ($error) use (&$serverListTab) {
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
? $error->getMessage()
: ((string) $error);
$serverListTab->statusLabel->setText('Async Fehler: ' . $errorMsg);
echo "Async error: {$errorMsg}\n";
$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 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"'));
$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 [
'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';
}
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']) {
$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
{
return $this->tab;
}
public function getSftpButton(): Button
{
return $this->sftpButton;
}
public function renderNeedsRebootCell(array $rowData, int $rowIndex): Label
{
$value = (string) ($rowData['needs_reboot'] ?? '');
$normalized = strtolower(trim($value));
$baseStyle = 'px-4 border-r border-gray-300 text-sm ';
if ($normalized === 'nein') {
$style = $baseStyle . 'text-green-600';
} elseif ($normalized === 'ja') {
$style = $baseStyle . 'text-red-600';
} else {
$style = $baseStyle . 'text-gray-600';
}
return new Label($value, $style);
}
public function renderUpdatesCell(array $rowData, int $rowIndex): Label
{
$value = (string) ($rowData['updates_available'] ?? '');
$normalized = strtolower(trim($value));
$baseStyle = 'px-4 border-r border-gray-300 text-sm ';
if ($normalized === 'nein' || $normalized === 'nein (0)') {
$style = $baseStyle . 'text-green-600';
} elseif (str_starts_with($normalized, 'ja')) {
$style = $baseStyle . 'text-red-600';
} else {
$style = $baseStyle . 'text-gray-600';
}
return new Label($value, $style);
}
private function updateDomainDetails(array $domains): void
{
$this->detailDomainsContainer->clearChildren();
if (empty($domains)) {
$this->detailDomainsContainer->addComponent(new Label(
'Keine Domains gefunden',
'text-xs text-gray-500 italic',
));
return;
}
foreach ($domains as $domain) {
if (!is_string($domain) || trim($domain) === '') {
continue;
}
$domain = trim($domain);
$button = new Button($domain, 'text-sm text-blue-600 hover:text-blue-800 underline text-left');
$button->setOnClick(function () use ($domain) {
$url = $domain;
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
$url = 'https://' . $url;
}
// Versuche, die Domain im Standardbrowser zu öffnen (Linux-Umgebung)
$escapedUrl = escapeshellarg($url);
exec("xdg-open {$escapedUrl} > /dev/null 2>&1 &");
});
$this->detailDomainsContainer->addComponent($button);
}
}
public function renderSelectCell(array $rowData, int $rowIndex): Checkbox
{
$isChecked = (bool) ($rowData['selected'] ?? false);
$checkbox = new Checkbox('', $isChecked);
$serverListTab = $this;
$rowId = $rowData['id'] ?? null;
$checkbox->setOnChange(function (bool $checked) use (&$serverListTab, &$rowData, $rowId) {
if ($rowId === null) {
return;
}
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;
}
}