diff --git a/services/funcs/funcs-service.yaml b/services/funcs/funcs-service.yaml index 8cf5cd5..586f6fc 100644 --- a/services/funcs/funcs-service.yaml +++ b/services/funcs/funcs-service.yaml @@ -27,7 +27,7 @@ spec: spec: containers: - name: funcs - image: naeel/sless-funcs-service:v0.1.8 + image: naeel/sless-funcs-service:v0.1.9 ports: - containerPort: 8090 env: diff --git a/services/funcs/index.html b/services/funcs/index.html index 7ad3d4d..e5ac867 100644 --- a/services/funcs/index.html +++ b/services/funcs/index.html @@ -188,15 +188,21 @@ color: #8b949e; } + .fn-url-row { + padding: 0 14px 9px 14px; + display: flex; + align-items: center; + gap: 6px; + } + .fn-url { font-size: 0.76rem; color: #58a6ff; text-decoration: none; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 280px; - flex-shrink: 1; + white-space: nowrap; + max-width: 100%; } .fn-url:hover { @@ -620,35 +626,47 @@ // Для Building/Pending — таймер секунд + автополинг до Ready/Failed if (fn.phase === 'Building' || fn.phase === 'Pending') { var timerEl = $txt(hdr, 'span', 'fn-timer', '0с'); - startBuildPoll(hdr, phaseBadge, timerEl, fn, ns); + startBuildPoll(phaseBadge, timerEl, fn, ns, null, function(url) { + if (!fn.url) { fn.url = url; setCardUrl(url); } + }); } - if (fn.url) { + if (fn.message) $txt(hdr, 'span', 'fn-msg', fn.message); + hdr.appendChild($el('span', 'spacer')); + + // urlRow — отдельная строка под заголовком для URL (не умещается в одну строку) + var urlRow = null; + var cardUrlAnchor = null; + + function ensureUrlRow() { + if (!urlRow) { + urlRow = $el('div', 'fn-url-row'); + // вставляем после hdr, до body + card.insertBefore(urlRow, body); + } + return urlRow; + } + + function setCardUrl(url) { + if (cardUrlAnchor) return; // уже есть + var row = ensureUrlRow(); var a = document.createElement('a'); a.className = 'fn-url'; - a.href = fn.url; - a.target = '_blank'; - a.rel = 'noopener noreferrer'; - a.textContent = fn.url; + a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer'; + a.textContent = url; a.addEventListener('click', function (e) { e.stopPropagation(); }); - hdr.appendChild(a); + row.appendChild(a); + cardUrlAnchor = a; } + if (fn.url) setCardUrl(fn.url); + for (var t = 0; t < triggers.length; t++) { var tr = triggers[t]; if (tr.type === 'http') { if (!fn.url) { var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || ''); - if (url) { - var a = document.createElement('a'); - a.className = 'fn-url'; - a.href = url; - a.target = '_blank'; - a.rel = 'noopener noreferrer'; - a.textContent = url; - a.addEventListener('click', function (e) { e.stopPropagation(); }); - hdr.appendChild(a); - } + if (url) setCardUrl(url); } hdr.appendChild(makeTriggerBtn(tr, ns)); } else if (tr.type === 'cron') { @@ -657,9 +675,6 @@ } } - if (fn.message) $txt(hdr, 'span', 'fn-msg', fn.message); - hdr.appendChild($el('span', 'spacer')); - // Кнопка редактирования кода var editBtn = $el('button', 'action-btn'); editBtn.textContent = '✎'; @@ -714,6 +729,7 @@ }); card.appendChild(hdr); + // urlRow вставляется динамически (ensureUrlRow) — до body card.appendChild(body); return card; } @@ -841,9 +857,16 @@ deps_filename: depsFilename || '' }) }).then(function (r) { + saveBtn.disabled = false; if (r.ok) { - status.textContent = '✓ Сохранено — сборка запущена'; - status.className = 'edit-status ok'; + // Ищем phaseBadge текущей карточки через DOM + var cardEl = body.closest ? body.closest('.fn-card') : null; + var pb = cardEl ? cardEl.querySelector('.badge-Ready, .badge-Building, .badge-Pending, .badge-Failed, .badge-Error') : null; + if (pb) { pb.className = 'badge badge-Building'; pb.textContent = 'Building'; } + status.textContent = '⏳ Сборка… 0с'; + status.className = 'edit-status'; + // Запускаем поллинг: обновляет статус в строке редактора и badge карточки + startBuildPoll(pb, null, { name: fnName, kind: fnKind || 'function' }, ns, status, null); } else { return r.text().then(function (t) { status.textContent = '✗ ' + t; @@ -851,9 +874,10 @@ }); } }).catch(function (err) { + saveBtn.disabled = false; status.textContent = '✗ ' + err; status.className = 'edit-status err'; - }).finally(function () { saveBtn.disabled = false; }); + }); }); cancelBtn.addEventListener('click', function () { body.style.display = 'none'; }); @@ -892,11 +916,14 @@ } // startBuildPoll — опрашивает /fn-status каждые 5с пока phase=Building/Pending. - // Показывает таймер в секундах, обновляет badge, добавляет URL когда сервис готов. - function startBuildPoll(hdr, phaseBadge, timerEl, fn, ns) { + // Показывает таймер-строку в statusEl (если передан), обновляет phaseBadge, + // добавляет URL через onUrl-коллбэк когда сервис появляется. + function startBuildPoll(phaseBadge, timerEl, fn, ns, statusEl, onUrl) { var buildStart = Date.now(); var timerItv = setInterval(function () { - timerEl.textContent = Math.floor((Date.now() - buildStart) / 1000) + 'с'; + var sec = Math.floor((Date.now() - buildStart) / 1000); + if (timerEl && timerEl.parentNode) timerEl.textContent = sec + 'с'; + if (statusEl) statusEl.textContent = '⏳ Сборка… ' + sec + 'с'; }, 1000); var pollItv = setInterval(function () { fetch('/funcs/' + ns + '/fn-status/' + fn.name + '?kind=' + (fn.kind || 'function')) @@ -905,25 +932,20 @@ if (!d.phase || d.phase === 'Building' || d.phase === 'Pending') return; clearInterval(timerItv); clearInterval(pollItv); - phaseBadge.className = 'badge badge-' + d.phase; - phaseBadge.textContent = d.phase; - // Убираем таймер с экрана — он нужен только во время сборки - if (timerEl.parentNode) timerEl.parentNode.removeChild(timerEl); - // Для сервиса — добавляем URL в заголовок карточки при появлении - if (d.url && !fn.url) { - fn.url = d.url; - var urlEl = document.createElement('a'); - urlEl.className = 'fn-url'; - urlEl.href = d.url; - urlEl.target = '_blank'; - urlEl.rel = 'noopener noreferrer'; - urlEl.textContent = d.url; - urlEl.addEventListener('click', function (e) { e.stopPropagation(); }); - var spacer = hdr.querySelector('.spacer'); - if (spacer) hdr.insertBefore(urlEl, spacer); + if (phaseBadge) { + phaseBadge.className = 'badge badge-' + d.phase; + phaseBadge.textContent = d.phase; } + if (timerEl && timerEl.parentNode) timerEl.parentNode.removeChild(timerEl); + if (statusEl) { + statusEl.textContent = d.phase === 'Ready' + ? '✓ Сборка завершена — функция готова' + : '✗ Сборка завершилась со статусом: ' + d.phase; + statusEl.className = 'edit-status ' + (d.phase === 'Ready' ? 'ok' : 'err'); + } + if (d.url && onUrl) onUrl(d.url); }) - .catch(function () {}); // тихо — нет смысла шуметь + .catch(function () {}); }, 5000); }