255 lines
11 KiB
JavaScript
255 lines
11 KiB
JavaScript
/* modals.js — управление модальными окнами Help и Invoke */
|
||
|
||
// makeDraggable — делает .panel внутри modalId перетаскиваемым за заголовок (h3).
|
||
function makeDraggable(modalId) {
|
||
var modal = document.getElementById(modalId);
|
||
if (!modal) return;
|
||
var panel = modal.querySelector('.panel');
|
||
if (!panel) return;
|
||
var handle = panel.querySelector('h3');
|
||
if (!handle) return;
|
||
|
||
handle.style.cursor = 'move';
|
||
handle.style.userSelect = 'none';
|
||
|
||
var startX, startY, startLeft, startTop;
|
||
|
||
handle.addEventListener('mousedown', function(e) {
|
||
e.preventDefault();
|
||
var rect = panel.getBoundingClientRect();
|
||
// Переводим в position:fixed если ещё не
|
||
if (!panel.style.left) {
|
||
panel.style.position = 'fixed';
|
||
panel.style.left = rect.left + 'px';
|
||
panel.style.top = rect.top + 'px';
|
||
panel.style.margin = '0';
|
||
}
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
startLeft = parseInt(panel.style.left, 10) || rect.left;
|
||
startTop = parseInt(panel.style.top, 10) || rect.top;
|
||
|
||
function onMove(e) {
|
||
var dx = e.clientX - startX;
|
||
var dy = e.clientY - startY;
|
||
panel.style.left = (startLeft + dx) + 'px';
|
||
panel.style.top = (startTop + dy) + 'px';
|
||
}
|
||
function onUp() {
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
}
|
||
document.addEventListener('mousemove', onMove);
|
||
document.addEventListener('mouseup', onUp);
|
||
});
|
||
}
|
||
|
||
// Инициализация после загрузки DOM
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal', 'logs-modal', 'info-modal'].forEach(makeDraggable);
|
||
|
||
// ESC закрывает активное модальное окно
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key !== 'Escape') return;
|
||
var modals = ['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal', 'logs-modal', 'info-modal'];
|
||
for (var i = 0; i < modals.length; i++) {
|
||
var el = document.getElementById(modals[i]);
|
||
if (el && el.classList.contains('open')) {
|
||
el.classList.remove('open');
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// showModalError — показывает ошибку внутри модалки.
|
||
// errorDivId — id элемента с классом modal-error.
|
||
function showModalError(errorDivId, message) {
|
||
var el = document.getElementById(errorDivId);
|
||
if (!el) return;
|
||
el.textContent = message;
|
||
el.style.display = 'block';
|
||
}
|
||
|
||
function hideModalError(errorDivId) {
|
||
var el = document.getElementById(errorDivId);
|
||
if (el) { el.style.display = 'none'; el.textContent = ''; }
|
||
}
|
||
|
||
function openHelp() {
|
||
document.getElementById('help-modal').classList.add('open');
|
||
}
|
||
|
||
function closeHelp() {
|
||
document.getElementById('help-modal').classList.remove('open');
|
||
}
|
||
|
||
function openInvoke(name) {
|
||
S.currentInvoke = name;
|
||
document.getElementById('i-title').textContent = 'Вызов: ' + name;
|
||
document.getElementById('i-resp').value = '';
|
||
document.getElementById('i-status').textContent = '';
|
||
document.getElementById('i-meta').style.display = 'none';
|
||
document.getElementById('i-meta').textContent = '';
|
||
document.getElementById('invoke-modal').classList.add('open');
|
||
}
|
||
|
||
function closeInvoke() {
|
||
document.getElementById('invoke-modal').classList.remove('open');
|
||
S.currentInvoke = null;
|
||
}
|
||
|
||
async function submitInvoke() {
|
||
if (!S.currentInvoke) return;
|
||
const btn = document.getElementById('i-submit');
|
||
const statusEl = document.getElementById('i-status');
|
||
const metaEl = document.getElementById('i-meta');
|
||
const respEl = document.getElementById('i-resp');
|
||
btn.disabled = true;
|
||
respEl.value = '';
|
||
metaEl.style.display = 'none';
|
||
metaEl.textContent = '';
|
||
let elapsed = 0;
|
||
statusEl.textContent = 'Вызов...';
|
||
const timer = setInterval(() => {
|
||
elapsed++;
|
||
if (elapsed < 5) {
|
||
statusEl.textContent = 'Вызов... ' + elapsed + 'с';
|
||
} else if (elapsed < 10) {
|
||
statusEl.textContent = '⏳ Возможен cold start — прогрев пула... ' + elapsed + 'с';
|
||
} else {
|
||
statusEl.textContent = '⏳ Возможны cold start или specialization... ' + elapsed + 'с';
|
||
}
|
||
}, 1000);
|
||
try {
|
||
const raw = document.getElementById('i-body').value.trim();
|
||
let parsed = {};
|
||
if (raw) parsed = JSON.parse(raw);
|
||
const result = await requestJSON(API_BASE + '/functions/' + encodeURIComponent(S.currentInvoke) + '/invoke', 'POST', parsed);
|
||
var latencyMs = Number(result.latency_ms || 0);
|
||
var measuredSeconds = latencyMs > 0 ? Math.max(1, Math.round(latencyMs / 1000)) : elapsed;
|
||
statusEl.textContent = '✓ Выполнено за ' + formatSeconds(measuredSeconds);
|
||
metaEl.style.display = 'block';
|
||
metaEl.textContent = [
|
||
'HTTP status: ' + (result.status || 'n/a'),
|
||
'Latency: ' + (latencyMs || 'n/a') + ' ms',
|
||
'Причина задержки: ' + explainDelay(measuredSeconds)
|
||
].join('\n');
|
||
respEl.value = JSON.stringify(result, null, 2);
|
||
} catch (e) {
|
||
statusEl.textContent = '✗ Ошибка после ' + formatSeconds(elapsed);
|
||
metaEl.style.display = 'block';
|
||
metaEl.textContent = [
|
||
'Последняя ошибка: ' + e.message,
|
||
'Вероятная причина задержки: ' + explainDelay(elapsed)
|
||
].join('\n');
|
||
respEl.value = 'Ошибка вызова: ' + e.message;
|
||
} finally {
|
||
clearInterval(timer);
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// openLogs — открывает модалку с логами функции.
|
||
async function openLogs(name) {
|
||
S.currentLogs = name;
|
||
document.getElementById('logs-title').textContent = 'Логи: ' + name;
|
||
document.getElementById('logs-output').value = 'Загрузка...';
|
||
document.getElementById('logs-modal').classList.add('open');
|
||
await _fetchLogs(name);
|
||
}
|
||
|
||
function closeLogs() {
|
||
document.getElementById('logs-modal').classList.remove('open');
|
||
S.currentLogs = null;
|
||
}
|
||
|
||
async function refreshLogs() {
|
||
if (S.currentLogs) await _fetchLogs(S.currentLogs);
|
||
}
|
||
|
||
async function _fetchLogs(name) {
|
||
var out = document.getElementById('logs-output');
|
||
try {
|
||
var data = await getJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/logs');
|
||
out.value = data.logs || '(нет логов)';
|
||
// прокручиваем вниз
|
||
out.scrollTop = out.scrollHeight;
|
||
} catch (e) {
|
||
out.value = 'Ошибка загрузки логов: ' + e.message;
|
||
}
|
||
}
|
||
|
||
// openInfo — открывает модалку с подробной информацией о функции.
|
||
// Данные берутся из data-атрибута кнопки (без дополнительного запроса к API).
|
||
function openInfo(name, btn) {
|
||
var raw = btn ? btn.getAttribute('data-info') : null;
|
||
var info = null;
|
||
try { info = raw ? JSON.parse(raw) : null; } catch(e) {}
|
||
if (!info) {
|
||
document.getElementById('info-rows').innerHTML = '<tr><td colspan="2">Нет данных</td></tr>';
|
||
document.getElementById('info-title').textContent = 'Информация: ' + name;
|
||
document.getElementById('info-modal').classList.add('open');
|
||
return;
|
||
}
|
||
|
||
// copyBtn — кнопка копирования конкретного значения через data-атрибут (безопасно для любых символов).
|
||
function copyBtn(val) {
|
||
if (!val || val === '-') return '';
|
||
var encoded = val.replace(/&/g, '&').replace(/"/g, '"');
|
||
return ' <button class="btn ghost" style="padding:1px 6px; font-size:11px;" data-copy="' + encoded + '" onclick="navigator.clipboard.writeText(this.dataset.copy)">📋</button>';
|
||
}
|
||
|
||
// Строки без кнопки копирования (только отображение).
|
||
function plain(val) {
|
||
return '<td class="mono" style="word-break:break-all;padding:4px 0;">' + h(val || '-') + '</td>';
|
||
}
|
||
|
||
// Строки с кнопкой копирования (Имя, Пакет, Маршрут — генерируемые или идентификаторы).
|
||
function withCopy(val) {
|
||
return '<td class="mono" style="word-break:break-all;padding:4px 0;">' + h(val || '-') + copyBtn(val) + '</td>';
|
||
}
|
||
|
||
function labelTd(label) {
|
||
return '<td style="color:var(--text-secondary);padding:4px 10px 4px 0;white-space:nowrap;vertical-align:top;">' + h(label) + '</td>';
|
||
}
|
||
|
||
// CURL-строка для внешнего вызова (полная, с токеном из localStorage).
|
||
var token = localStorage.getItem('auth_token') || '';
|
||
var method = (Array.isArray(info.methods) && info.methods.length) ? info.methods[0] : 'GET';
|
||
var externalUrl = window.location.origin + '/fn' + (info.route !== '-' ? info.route : '');
|
||
var curlFull = 'curl -H "X-Auth-Token: ' + token + '" -X ' + method + ' "' + externalUrl + '"';
|
||
// В отображении скрываем токен — показываем только метод и URL.
|
||
var curlDisplay = 'curl ... -X ' + method + ' "' + externalUrl + '"';
|
||
// Храним полную команду в data-атрибуте (HTML-encode), читаем через dataset — безопасно для любых символов.
|
||
var curlAttr = curlFull.replace(/&/g, '&').replace(/"/g, '"');
|
||
|
||
var html = [
|
||
'<tr>' + labelTd('Имя') + withCopy(info.name) + '</tr>',
|
||
'<tr>' + labelTd('Окружение') + plain(info.env) + '</tr>',
|
||
'<tr>' + labelTd('Пакет') + withCopy(info.pkg) + '</tr>',
|
||
'<tr>' + labelTd('Entrypoint') + plain(info.entrypoint) + '</tr>',
|
||
'<tr>' + labelTd('Таймаут') + plain(info.timeout ? info.timeout + ' сек' : '-') + '</tr>',
|
||
'<tr>' + labelTd('Тип источника') + plain(info.sourceType === 'archive' ? '📦 archive' : '📝 code') + '</tr>',
|
||
'<tr>' + labelTd('Маршрут') + withCopy(info.route) + '</tr>',
|
||
'<tr>' + labelTd('Методы') + plain(Array.isArray(info.methods) && info.methods.length ? info.methods.join(', ') : '-') + '</tr>',
|
||
'<tr>' + labelTd('Cron') + plain(info.cron || '-') + '</tr>',
|
||
'<tr>' + labelTd('Создана') + plain(info.createdAt) + '</tr>',
|
||
'<tr>' + labelTd('Изменена') + plain(info.updatedAt) + '</tr>',
|
||
'<tr>' + labelTd('curl') +
|
||
'<td class="mono" style="word-break:break-all;padding:4px 0;color:var(--text-secondary);">' +
|
||
h(curlDisplay) +
|
||
' <button class="btn ghost" style="padding:1px 6px; font-size:11px;" data-curl="' + curlAttr + '" onclick="navigator.clipboard.writeText(this.dataset.curl)">📋</button>' +
|
||
'</td></tr>',
|
||
].join('');
|
||
|
||
document.getElementById('info-title').textContent = 'Информация: ' + name;
|
||
document.getElementById('info-rows').innerHTML = html;
|
||
document.getElementById('info-modal').classList.add('open');
|
||
}
|
||
|
||
// closeInfo — закрывает Info-модалку.
|
||
function closeInfo() {
|
||
document.getElementById('info-modal').classList.remove('open');
|
||
}
|