This commit is contained in:
Thomas Peterson 2025-11-11 12:42:37 +01:00
parent 402ad74582
commit 2d631411cb
4 changed files with 278 additions and 27 deletions

View File

@ -66,7 +66,10 @@ $mainContainer = new Container('flex flex-col bg-gray-100');
// Modal dialog setup (hidden by default) // Modal dialog setup (hidden by default)
$apiKeyInput = new TextInput('API Key', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black'); $apiKeyInput = new TextInput('API Key', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black');
$privateKeyPathInput = new TextInput('Private Key Path', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black'); $privateKeyPathInput = new TextInput(
'Private Key Path',
'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black',
);
$modalDialog = new Container('bg-white rounded-lg p-6 flex flex-col w-96 gap-3'); $modalDialog = new Container('bg-white rounded-lg p-6 flex flex-col w-96 gap-3');
$modalDialog->addComponent(new Label('API Einstellungen', 'text-xl font-bold text-black')); $modalDialog->addComponent(new Label('API Einstellungen', 'text-xl font-bold text-black'));
@ -115,7 +118,14 @@ $fileMenu->addItem('Beenden', function () use ($app) {
// Settings Menu // Settings Menu
$settingsMenu = new Menu(title: 'Einstellungen'); $settingsMenu = new Menu(title: 'Einstellungen');
$settingsMenu->addItem('Optionen', function () use ($menuBar, $modal, $apiKeyInput, $privateKeyPathInput, &$currentApiKey, &$currentPrivateKeyPath) { $settingsMenu->addItem('Optionen', function () use (
$menuBar,
$modal,
$apiKeyInput,
$privateKeyPathInput,
&$currentApiKey,
&$currentPrivateKeyPath,
) {
$menuBar->closeAllMenus(); $menuBar->closeAllMenus();
$apiKeyInput->setValue($currentApiKey); $apiKeyInput->setValue($currentApiKey);
$privateKeyPathInput->setValue($currentPrivateKeyPath); $privateKeyPathInput->setValue($currentPrivateKeyPath);
@ -147,6 +157,10 @@ $refreshButton->setIcon($refreshIcon);
$leftSide->addComponent($refreshButton); $leftSide->addComponent($refreshButton);
// Search input field
$searchInput = new TextInput('Suche...', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black mb-2');
$leftSide->addComponent($searchInput);
$table = new Table(style: ' flex-1'); // flex-1 to fill available height but not expand infinitely $table = new Table(style: ' flex-1'); // flex-1 to fill available height but not expand infinitely
$table->setColumns([ $table->setColumns([
@ -168,7 +182,26 @@ for ($i = 1; $i <= 63; $i++) {
'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255), 'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255),
]; ];
} }
$table->setData($testData);
// Store current server data (will be updated when API loads servers)
$currentServerData = $testData;
$table->setData($currentServerData);
// Add search functionality
$searchInput->setOnChange(function ($value) use ($table, &$currentServerData) {
$searchTerm = strtolower(trim($value));
if (empty($searchTerm)) {
// Show all data if search is empty
$table->setData($currentServerData);
} else {
// Filter by name
$filteredData = array_filter($currentServerData, function ($row) use ($searchTerm) {
return str_contains(strtolower($row['name']), $searchTerm);
});
$table->setData(array_values($filteredData));
}
});
$leftSide->addComponent($table); $leftSide->addComponent($table);
$tab1->addComponent($leftSide); $tab1->addComponent($leftSide);
@ -197,11 +230,71 @@ $detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($detailIpv4); $detailPanel->addComponent($detailIpv4);
// SFTP Manager Button // SFTP Manager Button
$sftpButton = new Button('SFTP Manager öffnen', 'w-full mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center'); $sftpButton = new Button(
'SFTP Manager öffnen',
'w-full mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center',
);
$sftpIcon = new Icon(IconName::home, 18, 'text-white mr-2'); $sftpIcon = new Icon(IconName::home, 18, 'text-white mr-2');
$sftpButton->setIcon($sftpIcon); $sftpButton->setIcon($sftpIcon);
// Add synchronous click handler to switch tab immediately
$sftpButton->setOnClick(function () use ($tabContainer) {
$tabContainer->setActiveTab(3); // Switch to SFTP Manager tab immediately
});
$detailPanel->addComponent($sftpButton); $detailPanel->addComponent($sftpButton);
// SSH Terminal Button
$sshTerminalButton = new Button(
'SSH Terminal öffnen',
'w-full mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center justify-center',
);
$sshTerminalIcon = new Icon(IconName::home, 18, 'text-white mr-2');
$sshTerminalButton->setIcon($sshTerminalIcon);
// Add click handler to open SSH terminal
$sshTerminalButton->setOnClick(function () use (&$selectedServer, &$currentPrivateKeyPath, &$statusLabel) {
if ($selectedServer === null) {
$statusLabel->setText('Kein Server ausgewählt');
return;
}
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
$statusLabel->setText('Private Key Pfad nicht konfiguriert');
return;
}
// Build SSH command with private key
$host = $selectedServer['ipv4'];
$keyPath = escapeshellarg($currentPrivateKeyPath);
$sshCommand = "ssh -i {$keyPath} root@{$host}";
// Try to open terminal with SSH connection
// Different terminal emulators for different systems
$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;
$statusLabel->setText('SSH Terminal geöffnet für ' . $selectedServer['name']);
break;
}
}
if (!$opened) {
$statusLabel->setText('Konnte kein Terminal öffnen. SSH Befehl: ' . $sshCommand);
}
});
$detailPanel->addComponent($sshTerminalButton);
$tab1->addComponent($detailPanel); $tab1->addComponent($detailPanel);
// Row selection handler - update detail panel // Row selection handler - update detail panel
@ -209,7 +302,15 @@ $statusLabel = new Label(
text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight, text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
style: 'basis-4/8 text-black', style: 'basis-4/8 text-black',
); );
$table->setOnRowSelect(function ($index, $row) use (&$statusLabel, &$selectedServer, $detailId, $detailName, $detailStatus, $detailType, $detailIpv4) { $table->setOnRowSelect(function ($index, $row) use (
&$statusLabel,
&$selectedServer,
$detailId,
$detailName,
$detailStatus,
$detailType,
$detailIpv4,
) {
if ($row) { if ($row) {
$statusLabel->setText("Server: {$row['name']} - {$row['status']} ({$row['ipv4']})"); $statusLabel->setText("Server: {$row['name']} - {$row['status']} ({$row['ipv4']})");
@ -260,14 +361,29 @@ $loadServersAsync = function () use ($currentApiKey) {
// Configure refresh button to load servers asynchronously // Configure refresh button to load servers asynchronously
$refreshButton->setOnClickAsync( $refreshButton->setOnClickAsync(
$loadServersAsync, $loadServersAsync,
function ($result) use ($table, $statusLabel) { function ($result) use ($table, $statusLabel, &$currentServerData, $searchInput) {
// Handle the result in the main thread (can access objects here) // Handle the result in the main thread (can access objects here)
if (is_array($result)) { if (is_array($result)) {
if (isset($result['error'])) { if (isset($result['error'])) {
$statusLabel->setText('Fehler: ' . $result['error']); $statusLabel->setText('Fehler: ' . $result['error']);
echo "Error: {$result['error']}\n"; echo "Error: {$result['error']}\n";
} elseif (isset($result['success'], $result['servers'])) { } elseif (isset($result['success'], $result['servers'])) {
$table->setData($result['servers']); // Update current server data
$currentServerData = $result['servers'];
// Check if search is active
$searchTerm = strtolower(trim($searchInput->getValue()));
if (empty($searchTerm)) {
// No search, show all servers
$table->setData($currentServerData);
} else {
// Apply search filter to new data
$filteredData = array_filter($currentServerData, function ($row) use ($searchTerm) {
return str_contains(strtolower($row['name']), $searchTerm);
});
$table->setData(array_values($filteredData));
}
$statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); $statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
echo "Success: {$result['count']} servers loaded\n"; echo "Success: {$result['count']} servers loaded\n";
} }
@ -282,19 +398,7 @@ $refreshButton->setOnClickAsync(
$tabContainer->addTab('Server', $tab1); $tabContainer->addTab('Server', $tab1);
// Tab 2: Some info // Tab 2: SFTP Manager (will be populated dynamically when server is selected)
$tab2 = new Container('flex flex-col p-4');
$tab2->addComponent(new Label('Dies ist Tab 2', 'text-xl font-bold mb-4'));
$tab2->addComponent(new Label('Hier könnte weiterer Inhalt stehen...', ''));
$tabContainer->addTab('Info', $tab2);
// Tab 3: Settings
$tab3 = new Container('flex flex-col p-4');
$tab3->addComponent(new Label('Einstellungen', 'text-xl font-bold mb-4'));
$tab3->addComponent(new Label('Konfigurationsoptionen...', ''));
$tabContainer->addTab('Einstellungen', $tab3);
// Tab 4: SFTP Manager (will be populated dynamically when server is selected)
$sftpTab = new Container('flex flex-row p-4 gap-4 bg-gray-50'); $sftpTab = new Container('flex flex-row p-4 gap-4 bg-gray-50');
// Left side: Local file browser // Left side: Local file browser
@ -318,6 +422,80 @@ $sftpTab->addComponent($remoteBrowserContainer);
$tabContainer->addTab('SFTP Manager', $sftpTab); $tabContainer->addTab('SFTP Manager', $sftpTab);
// Remote FileBrowser navigation handler - load directory asynchronously
$remoteFileBrowser->setOnFileSelect(function ($path, $row) use (
$remoteFileBrowser,
&$selectedServer,
&$currentPrivateKeyPath,
&$statusLabel,
) {
if (!isset($row['isDir']) || !$row['isDir']) {
return; // Only handle directories
}
// Load directory asynchronously via Button with async handler
$loadButton = new Button('Load', '');
$loadButton->setOnClickAsync(
function () use ($path, &$selectedServer, &$currentPrivateKeyPath) {
if ($selectedServer === null || empty($currentPrivateKeyPath)) {
return ['error' => 'Not connected'];
}
try {
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
if (!$sftp->login('root', $key)) {
return ['error' => 'SFTP Login failed'];
}
$files = $sftp->nlist($path);
if ($files === false) {
return ['error' => 'Cannot read directory'];
}
$fileList = [];
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$fullPath = rtrim($path, '/') . '/' . $file;
$stat = $sftp->stat($fullPath);
$fileList[] = [
'name' => $file,
'path' => $fullPath,
'isDir' => ($stat['type'] ?? 0) === 2,
'size' => $stat['size'] ?? 0,
'mtime' => $stat['mtime'] ?? 0,
];
}
return ['success' => true, 'path' => $path, 'files' => $fileList];
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
},
function ($result) use ($remoteFileBrowser, &$statusLabel) {
if (isset($result['error'])) {
$statusLabel->setText('SFTP Fehler: ' . $result['error']);
return;
}
if (isset($result['success'])) {
$remoteFileBrowser->setPath($result['path']);
$remoteFileBrowser->setFileData($result['files']);
}
},
function ($error) use (&$statusLabel) {
$errorMsg = is_string($error) ? $error : 'Unknown error';
$statusLabel->setText('SFTP Error: ' . $errorMsg);
},
);
// Trigger the async load
$loadButton->handleMouseClick(0, 0, 0);
});
// SFTP Button Click Handler - Connect to server and load remote files // SFTP Button Click Handler - Connect to server and load remote files
$sftpButton->setOnClickAsync( $sftpButton->setOnClickAsync(
function () use (&$selectedServer, &$currentPrivateKeyPath) { function () use (&$selectedServer, &$currentPrivateKeyPath) {
@ -382,7 +560,9 @@ $sftpButton->setOnClickAsync(
} }
if (isset($result['success']) && $result['success']) { if (isset($result['success']) && $result['success']) {
$connectionStatusLabel->setText('Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')'); $connectionStatusLabel->setText(
'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')',
);
$statusLabel->setText('SFTP Verbindung erfolgreich zu ' . $result['server']['name']); $statusLabel->setText('SFTP Verbindung erfolgreich zu ' . $result['server']['name']);
// Populate remote file browser with the file list // Populate remote file browser with the file list
@ -390,11 +570,13 @@ $sftpButton->setOnClickAsync(
$remoteFileBrowser->setFileData($result['files']); $remoteFileBrowser->setFileData($result['files']);
// Switch to SFTP Manager tab // Switch to SFTP Manager tab
$tabContainer->setActiveTab(3); // Index 3 = SFTP Manager tab $tabContainer->setActiveTab(1); // Index 3 = SFTP Manager tab
} }
}, },
function ($error) use (&$statusLabel) { function ($error) use (&$statusLabel) {
$errorMsg = is_string($error) ? $error : (is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : 'Unbekannter Fehler'); $errorMsg = is_string($error)
? $error
: (is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : 'Unbekannter Fehler');
$statusLabel->setText('SFTP Async Fehler: ' . $errorMsg); $statusLabel->setText('SFTP Async Fehler: ' . $errorMsg);
}, },
); );
@ -424,7 +606,16 @@ $cancelButton->setOnClick(function () use ($menuBar, $modal) {
$modal->setVisible(false); $modal->setVisible(false);
}); });
$saveButton->setOnClick(function () use ($settings, &$currentApiKey, &$currentPrivateKeyPath, $apiKeyInput, $privateKeyPathInput, $menuBar, $modal, &$statusLabel) { $saveButton->setOnClick(function () use (
$settings,
&$currentApiKey,
&$currentPrivateKeyPath,
$apiKeyInput,
$privateKeyPathInput,
$menuBar,
$modal,
&$statusLabel,
) {
$currentApiKey = trim($apiKeyInput->getValue()); $currentApiKey = trim($apiKeyInput->getValue());
$currentPrivateKeyPath = trim($privateKeyPathInput->getValue()); $currentPrivateKeyPath = trim($privateKeyPathInput->getValue());

View File

@ -0,0 +1,30 @@
<?php
namespace PHPNative\Ui\Widget;
class ClickableHeaderLabel extends Label
{
private string $columnKey;
private Table $table;
public function __construct(string $columnKey, Table $table, string $title, string $style)
{
$this->columnKey = $columnKey;
$this->table = $table;
parent::__construct($title, $style);
}
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
if (
$mouseX >= $this->viewport->x &&
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
$mouseY >= $this->viewport->y &&
$mouseY <= ($this->viewport->y + $this->viewport->height)
) {
$this->table->sortByColumn($this->columnKey);
return true;
}
return parent::handleMouseClick($mouseX, $mouseY, $button);
}
}

View File

@ -269,5 +269,34 @@ class FileBrowser extends Container
} }
$this->fileTable->setData($tableData); $this->fileTable->setData($tableData);
// Set up row selection handler AFTER data is set (for remote browsers)
// This needs to be done every time because setData might reset handlers
$this->setupRemoteNavigationHandler();
}
/**
* Setup navigation handler for remote file browser
*/
private function setupRemoteNavigationHandler(): void
{
if (!$this->isRemote) {
return;
}
$fileBrowser = $this;
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) {
if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) {
// Trigger the external callback for directory navigation
if ($fileBrowser->onFileSelect !== null) {
($fileBrowser->onFileSelect)($row['path'], $row);
}
} elseif ($row && isset($row['path']) && !empty($row['path']) && !($row['isDir'] ?? false)) {
// File selected
if ($fileBrowser->onFileSelect !== null) {
($fileBrowser->onFileSelect)($row['path'], $row);
}
}
});
} }
} }

View File

@ -48,17 +48,18 @@ class Table extends Container
$this->headerContainer->clearChildren(); $this->headerContainer->clearChildren();
foreach ($columns as $column) { foreach ($columns as $column) {
$title = $column['title'] ?? $column['key']; $key = $column['key'];
$title = $column['title'] ?? $key;
$width = $column['width'] ?? null; $width = $column['width'] ?? null;
$style = 'px-4 py-2 text-black font-bold border-r border-gray-300'; $style = 'px-4 py-2 text-black font-bold border-r border-gray-300 hover:bg-gray-300 cursor-pointer';
if ($width) { if ($width) {
$style .= ' w-' . ((int) ($width / 4)); $style .= ' w-' . ((int) ($width / 4));
} else { } else {
$style .= ' flex-1'; $style .= ' flex-1';
} }
$headerLabel = new Label($title, $style); $headerLabel = new ClickableHeaderLabel($key, $this, $title, $style);
$this->headerContainer->addComponent($headerLabel); $this->headerContainer->addComponent($headerLabel);
} }
} }