Files
MageSail/Magesail/Block/Adminhtml/Tunnel.php
T
snxraven 24f58cb06c feat(admin): improve Holesail tunnel management UX
- Add Tunnels list / Add a tunnel tabs with hash navigation
- Show busy overlay during start/stop/remove/create AJAX
- Reload with #magesail-list after successful actions
- Only reload stop/remove on success; fix error handling
- Remove redundant Stores configuration link; drop getConfigUrl()
2026-03-21 01:50:24 -05:00

221 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace MageSail\Magesail\Block\Adminhtml;
use Magento\Backend\Block\Template;
use Magento\Backend\Block\Template\Context;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Store\Model\StoreManagerInterface;
use MageSail\Magesail\Model\TunnelRegistry;
use MageSail\Magesail\Model\TunnelStatus;
use MageSail\Magesail\Model\TunnelStoreProvisioner;
class Tunnel extends Template
{
private const LOG_TAIL_BYTES = 8192;
private const LOG_TAIL_LINES = 40;
private const XML_PORT = 'magesail/settings/port';
private const XML_MODE = 'magesail/settings/mode';
public function __construct(
Context $context,
private readonly TunnelStatus $tunnelStatus,
private readonly TunnelStoreProvisioner $tunnelStoreProvisioner,
private readonly StoreManagerInterface $storeManager,
private readonly ScopeConfigInterface $scopeConfig,
private readonly TunnelRegistry $tunnelRegistry,
private readonly Json $json,
array $data = []
) {
parent::__construct($context, $data);
}
/**
* @return list<array{tunnel_id: string, label: string, store_code: string, local_port: int, running: bool, pid: int|null, key: string|null}>
*/
public function getAllTunnelStatuses(): array
{
return $this->tunnelStatus->getAllTunnelStatuses();
}
/**
* @return list<array<string, mixed>>
*/
public function listTunnelsFromRegistry(): array
{
return $this->tunnelRegistry->listTunnels();
}
public function hasAnyNginxPendingReload(): bool
{
return $this->tunnelRegistry->anyTunnelHasNginxPendingReload();
}
public function getDefaultLocalPort(): int
{
$port = filter_var($this->scopeConfig->getValue(self::XML_PORT) ?: 80, FILTER_VALIDATE_INT);
return ($port && $port >= 1 && $port <= 65535) ? $port : 80;
}
/**
* Preset ports shown first in Admin (443 is typical for Holesail + HTTPS upstream).
*
* @return list<int>
*/
public function getLocalPortPresetValues(): array
{
return [443, 80, 8080];
}
/**
* Initial Add-tunnel UI: preset key '443'|'80'|'8080'|'custom' and optional custom port when not a preset.
*
* @return array{preset: string, custom_port: int}
*/
public function getInitialLocalPortUiState(): array
{
$presets = $this->getLocalPortPresetValues();
$cfg = $this->getDefaultLocalPort();
foreach ($presets as $p) {
if ($p === $cfg) {
return ['preset' => (string) $p, 'custom_port' => $cfg];
}
}
return ['preset' => 'custom', 'custom_port' => $cfg];
}
public function getDefaultHolesailSecure(): bool
{
return (bool) $this->scopeConfig->getValue(self::XML_MODE);
}
/**
* @return list<array{value: string, label: string}>
*/
public function getWebsiteOptions(): array
{
$options = [];
foreach ($this->storeManager->getWebsites() as $website) {
$options[] = [
'value' => (string) $website->getId(),
'label' => $website->getName() . ' (' . $website->getCode() . ')',
];
}
return $options;
}
/**
* JSON map: websiteId string => list of {id, name}
*/
public function getStoreGroupsByWebsiteJson(): string
{
$map = [];
foreach ($this->storeManager->getWebsites() as $website) {
$wid = (string) $website->getId();
$map[$wid] = [];
foreach ($website->getGroups() as $group) {
$map[$wid][] = [
'id' => (string) $group->getId(),
'name' => $group->getName() . ' (' . $group->getCode() . ')',
];
}
}
return $this->json->serialize($map);
}
/**
* @deprecated
*/
public function getTunnelStoreState(): ?array
{
return $this->tunnelStoreProvisioner->getPersistedState();
}
public function getTunnelStoreCodeConstant(): string
{
return TunnelStoreProvisioner::STORE_CODE;
}
/**
* Strip scheme for display so Admin shows hostname-only; persisted URLs stay full in state.
*/
public function getTunnelHostDisplay(?string $baseUrl): string
{
if ($baseUrl === null || $baseUrl === '') {
return '';
}
$parts = parse_url($baseUrl);
if ($parts === false || empty($parts['host'])) {
return $baseUrl;
}
$out = $parts['host'];
if (!empty($parts['port'])) {
$out .= ':' . $parts['port'];
}
if (!empty($parts['path']) && $parts['path'] !== '/' && $parts['path'] !== '') {
$out .= rtrim($parts['path'], '/');
}
return $out;
}
public function getStatusUrl(): string
{
return $this->getUrl('*/*/status');
}
public function getIndexUrl(): string
{
return $this->getUrl('*/*/index');
}
public function getLogtailUrl(): string
{
return $this->getUrl('*/*/logtail');
}
public function getFormKey(): string
{
return $this->formKey->getFormKey();
}
public function getLogFilePathForTunnel(string $tunnelId): string
{
return $this->tunnelStatus->getLogFilePath($tunnelId);
}
/**
* @return string[]
*/
public function getRecentLogLinesForTunnel(string $tunnelId): array
{
$path = $this->tunnelStatus->getLogFilePath($tunnelId);
if (!\is_readable($path)) {
return [];
}
$size = filesize($path);
if ($size === false || $size === 0) {
return [];
}
$fp = fopen($path, 'rb');
if ($fp === false) {
return [];
}
$start = max(0, $size - self::LOG_TAIL_BYTES);
fseek($fp, $start);
if ($start > 0) {
fgets($fp);
}
$chunk = stream_get_contents($fp) ?: '';
fclose($fp);
$lines = preg_split('/\R/', $chunk) ?: [];
$lines = array_values(array_filter($lines, static fn ($l) => $l !== ''));
if (\count($lines) > self::LOG_TAIL_LINES) {
$lines = \array_slice($lines, -self::LOG_TAIL_LINES);
}
return $lines;
}
}