Updates: Allow automatic display of keys/Copy button ack

This commit is contained in:
2026-03-12 17:41:54 -05:00
parent e5c229381b
commit db97652bc2
12 changed files with 518 additions and 182 deletions
@@ -1,17 +1,26 @@
<?php
/** @var \MageSail\Magesail\Block\Adminhtml\Tunnel $block */
$status = $block->getTunnelStatus();
$formKey = $block->getFormKey();
$statusUrl = $block->escapeUrl($block->getStatusUrl());
$formKey = $block->escapeHtml($block->getFormKey());
$statusUrl = $block->getStatusUrl();
$indexUrl = $block->getIndexUrl();
$configUrl = $block->escapeUrl($block->getConfigUrl());
$logPath = $block->escapeHtml($block->getLogFilePath());
$logLines = $block->getRecentLogLines();
$running = $status['running'];
?>
<div class="magesail-tunnel">
<div class="magesail-tunnel"
id="magesail-root"
data-status-url="<?= $block->escapeUrl($statusUrl) ?>"
data-index-url="<?= $block->escapeUrl($indexUrl) ?>"
data-logtail-url="<?= $block->escapeUrl($block->getLogtailUrl()) ?>"
data-form-key="<?= $formKey ?>">
<div class="page-title-wrapper">
<h1 class="page-title"><?= $block->escapeHtml(__('Holesail Tunnel Management')) ?></h1>
</div>
<div id="magesail-flash" class="message" style="display:none"></div>
<div class="admin__page-section">
<div class="admin__page-section-title">
<span class="title"><?= $block->escapeHtml(__('Tunnel status')) ?></span>
@@ -23,104 +32,46 @@ $logLines = $block->getRecentLogLines();
</a>
</p>
<?php if ($status['running']): ?>
<div class="message message-success success" id="magesail-status-banner">
<span id="tunnel-status-text">
<?= $block->escapeHtml(__('Tunnel is running (PID: %1).', $status['pid'])) ?>
</span>
<div id="magesail-stopped" class="magesail-panel" style="<?= $running ? 'display:none' : '' ?>">
<div class="message message-warning warning">
<span><?= $block->escapeHtml(__('No active tunnel. Start one to expose your dev site.')) ?></span>
</div>
<div class="magesail-actions">
<button type="button" class="action-primary" id="magesail-btn-start">
<span><?= $block->escapeHtml(__('Start Tunnel')) ?></span>
</button>
</div>
</div>
<div id="magesail-running" class="magesail-panel" style="<?= $running ? '' : 'display:none' ?>">
<div class="message message-success success" id="magesail-status-banner">
<span id="tunnel-status-text"><?php
if ($running && $status['pid']) {
echo $block->escapeHtml(__('Tunnel is running (PID: %1).', $status['pid']));
}
?></span>
</div>
<div class="magesail-key-wrap" id="magesail-key-section">
<strong><?= $block->escapeHtml(__('Share key')) ?></strong>
<code class="magesail-key" id="magesail-key-value"><?= $block->escapeHtml($status['key'] ?: '') ?></code>
<span class="magesail-generating" id="magesail-key-loading" style="<?= $status['key'] ? 'display:none' : '' ?>">
<span class="magesail-generating" id="magesail-key-loading" style="<?= ($running && !$status['key']) ? '' : 'display:none' ?>">
<?= $block->escapeHtml(__('Generating key…')) ?>
</span>
<button type="button" class="action-default" id="magesail-copy-key" <?= $status['key'] ? '' : 'disabled' ?>>
<button type="button" class="action-default" id="magesail-copy-key" data-original-label="<?= $block->escapeHtmlAttr(__('Copy key')) ?>" <?= ($status['key'] ?? '') ? '' : 'disabled' ?>>
<span><?= $block->escapeHtml(__('Copy key')) ?></span>
</button>
</div>
<p class="admin__field-note">
<?= $block->escapeHtml(__('Client example:')) ?>
<code id="magesail-client-cmd"><?= $block->escapeHtml($status['key'] ? 'holesail ' . $status['key'] : 'holesail hs://…') ?></code>
<button type="button" class="action-link" id="magesail-copy-cmd" <?= $status['key'] ? '' : 'disabled' ?>><?= $block->escapeHtml(__('Copy')) ?></button>
<button type="button" class="action-link" id="magesail-copy-cmd" data-original-label="<?= $block->escapeHtmlAttr(__('Copy')) ?>"><?= $block->escapeHtml(__('Copy')) ?></button>
</p>
<form method="post" action="<?= $block->escapeUrl($block->getUrl('*/*/index')) ?>" class="magesail-actions">
<?= $block->getBlockHtml('formkey') ?>
<input type="hidden" name="action" value="stop"/>
<button type="submit" class="action-primary action-stop">
<div class="magesail-actions">
<button type="button" class="action-primary action-stop" id="magesail-btn-stop">
<span><?= $block->escapeHtml(__('Stop Tunnel')) ?></span>
</button>
</form>
<script>
require(['jquery'], function ($) {
var statusUrl = <?= json_encode($statusUrl) ?>;
var pollTimer = null;
var pollIntervalMs = 3000;
function copyText(text) {
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text);
} else {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
}
$('#magesail-copy-key').on('click', function () {
copyText($('#magesail-key-value').text().trim());
});
$('#magesail-copy-cmd').on('click', function () {
copyText($('#magesail-client-cmd').text().trim());
});
function applyStatus(data) {
if (!data || data.error) return;
if (data.running && data.pid) {
$('#tunnel-status-text').text(
<?= json_encode((string) __('Tunnel is running (PID: %1).', '{{pid}}')) ?>.replace('{{pid}}', data.pid)
);
}
if (data.key && String(data.key).trim() !== '') {
var k = String(data.key).trim();
$('#magesail-key-value').text(k);
$('#magesail-client-cmd').text('holesail ' + k);
$('#magesail-key-loading').hide();
$('#magesail-copy-key, #magesail-copy-cmd').prop('disabled', false);
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
}
var keyReady = <?= $status['key'] ? 'true' : 'false' ?>;
if (!keyReady) {
pollTimer = setInterval(function () {
$.getJSON(statusUrl).done(applyStatus).fail(function () {});
}, pollIntervalMs);
}
});
</script>
<?php else: ?>
<div class="message message-warning warning">
<span><?= $block->escapeHtml(__('No active tunnel. Start one to expose your dev site.')) ?></span>
</div>
<form method="post" action="<?= $block->escapeUrl($block->getUrl('*/*/index')) ?>" class="magesail-actions">
<?= $block->getBlockHtml('formkey') ?>
<input type="hidden" name="action" value="start"/>
<button type="submit" class="action-primary action-start">
<span><?= $block->escapeHtml(__('Start Tunnel')) ?></span>
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
@@ -129,15 +80,204 @@ $logLines = $block->getRecentLogLines();
<span class="title"><?= $block->escapeHtml(__('Log tail (%1)', $logPath)) ?></span>
</div>
<div class="admin__page-section-content">
<?php if ($logLines): ?>
<pre class="magesail-log-preview" aria-label="<?= $block->escapeHtmlAttr(__('Recent log lines')) ?>"><?php
<pre class="magesail-log-preview" id="magesail-log-preview" aria-label="<?= $block->escapeHtmlAttr(__('Recent log lines')) ?>"><?php
if ($logLines) {
foreach ($logLines as $line) {
echo $block->escapeHtml($line) . "\n";
}
?></pre>
<?php else: ?>
<p class="admin__field-note"><?= $block->escapeHtml(__('No log output yet or log file not readable.')) ?></p>
<?php endif; ?>
} else {
echo $block->escapeHtml(__('No log output yet or log file not readable.'));
}
?></pre>
</div>
</div>
</div>
<script>
require(['jquery', 'domReady!'], function ($) {
var root = $('#magesail-root');
var statusUrl = root.data('status-url');
var indexUrl = root.data('index-url');
var formKey = root.data('form-key');
var pollTimer = null;
var logPollTimer = null;
var pidMsgTemplate = <?= json_encode((string) __('Tunnel is running (PID: %1).', '%1')) ?>;
var copiedLabel = <?= json_encode((string) __('Copied!')) ?>;
function flash(msg, isError) {
var el = $('#magesail-flash');
el.removeClass('message-success success message-error error message-warning warning')
.addClass(isError ? 'message-error error' : 'message-success success')
.html('<span>' + $('<div/>').text(msg).html() + '</span>')
.show();
if (!isError) {
setTimeout(function () { el.fadeOut(); }, 5000);
}
}
function copyText(text) {
if (!text) return false;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text);
} else {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
return true;
} catch (e) {
return false;
}
}
function copyFeedback(btn) {
var span = btn.find('span');
if (!span.length) span = btn;
var original = btn.data('original-label') || span.text();
btn.addClass('magesail-copied');
span.text(copiedLabel);
setTimeout(function () {
btn.removeClass('magesail-copied');
span.text(original);
}, 2000);
}
function applyStatus(data) {
if (!data || typeof data.running === 'undefined') return;
if (data.running && data.pid) {
$('#tunnel-status-text').text(pidMsgTemplate.replace('%1', data.pid));
}
if (data.key && String(data.key).trim() !== '') {
var k = String(data.key).trim();
$('#magesail-key-value').text(k);
$('#magesail-client-cmd').text('holesail ' + k);
$('#magesail-key-loading').hide();
$('#magesail-copy-key, #magesail-copy-cmd').prop('disabled', false);
stopPoll();
}
}
function stopPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPoll() {
stopPoll();
function tick() {
$.ajax({
url: statusUrl,
type: 'GET',
dataType: 'json',
xhrFields: { withCredentials: true }
}).done(applyStatus).fail(function (xhr) {
if (xhr.responseJSON && xhr.responseJSON.message) {
flash(xhr.responseJSON.message, true);
}
});
}
tick();
pollTimer = setInterval(tick, 2000);
}
function showStopped() {
$('#magesail-running').hide();
$('#magesail-stopped').show();
stopPoll();
$('#magesail-key-value').text('');
$('#magesail-client-cmd').text('holesail hs://…');
$('#magesail-key-loading').show();
$('#magesail-copy-key, #magesail-copy-cmd').prop('disabled', true);
}
function showRunning(status) {
$('#magesail-stopped').hide();
$('#magesail-running').show();
applyStatus(status || { running: true });
var hasKey = status && status.key && String(status.key).trim() !== '';
if (!hasKey) {
$('#magesail-key-loading').show();
$('#magesail-copy-key, #magesail-copy-cmd').prop('disabled', true);
startPoll();
} else {
$('#magesail-key-loading').hide();
stopPoll();
}
}
$('#magesail-copy-key').on('click', function () {
var text = $('#magesail-key-value').text().trim();
if (copyText(text)) {
copyFeedback($(this));
}
});
$('#magesail-copy-cmd').on('click', function () {
var text = $('#magesail-client-cmd').text().trim();
if (copyText(text)) {
copyFeedback($(this));
}
});
$('#magesail-btn-start').on('click', function () {
var btn = $(this).prop('disabled', true);
$.ajax({
url: indexUrl,
type: 'POST',
dataType: 'json',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
data: { form_key: formKey, action: 'start', ajax: '1' }
}).done(function (res) {
if (res.success && res.status) {
flash(res.message || '', false);
showRunning(res.status);
} else {
flash(res.message || 'Start failed', true);
}
}).fail(function (xhr) {
var m = (xhr.responseJSON && xhr.responseJSON.message) ? xhr.responseJSON.message : 'Request failed';
flash(m, true);
}).always(function () {
btn.prop('disabled', false);
});
});
$('#magesail-btn-stop').on('click', function () {
var btn = $(this).prop('disabled', true);
$.ajax({
url: indexUrl,
type: 'POST',
dataType: 'json',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
data: { form_key: formKey, action: 'stop', ajax: '1' }
}).done(function (res) {
flash(res.message || '', false);
showStopped();
}).fail(function () {
flash('Stop failed', true);
}).always(function () {
btn.prop('disabled', false);
});
});
if ($('#magesail-running').is(':visible') && !$('#magesail-key-value').text().trim()) {
startPoll();
}
var logUrl = root.data('logtail-url');
if (logUrl) {
logPollTimer = setInterval(function () {
$.getJSON(logUrl).done(function (res) {
if (res.lines && res.lines.length) {
$('#magesail-log-preview').text(res.lines.join('\n'));
}
});
}, 4000);
}
});
</script>
@@ -58,3 +58,11 @@
.magesail-tunnel .magesail-actions {
margin-top: 1rem;
}
.magesail-tunnel .magesail-copied {
background-color: #006400 !important;
border-color: #006400 !important;
color: #fff !important;
}
.magesail-tunnel .magesail-copied span {
color: #fff !important;
}