From 4960bf0ee28574f029fb419f013d24197110ae6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 2 May 2026 17:27:20 +0400 Subject: [PATCH] refactor(ui): step8 - extract functions.js (parseMethods, parseTimeout, onLangChange, openCreate/Edit/Invoke CRUD, removeFn) --- console/ui/index.html | 206 +------------------------------------ console/ui/js/functions.js | 152 +++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 205 deletions(-) create mode 100644 console/ui/js/functions.js diff --git a/console/ui/index.html b/console/ui/index.html index c5bb615..1e76ec3 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -19,6 +19,7 @@ + @@ -415,17 +416,6 @@ currentInvoke: null }; - function parseMethods(v) { - const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean); - return items.length ? Array.from(new Set(items)) : ['GET']; - } - - function parseTimeout(v) { - var n = Number(String(v || '').trim()); - if (!Number.isFinite(n) || n <= 0) return 60; - return Math.round(n); - } - function llmGeneratedWarning(lang) { var text = 'Сделано LLM. Не доверяй, проверяй!'; switch (lang) { @@ -454,200 +444,6 @@ return warning + '\n' + body; } - function onLangChange() { - var lang = document.getElementById('c-lang').value; - var t = LANG_TEMPLATES[lang]; - if (t) { - document.getElementById('c-entry').value = t.entrypoint; - document.getElementById('c-code').value = t.code; - } - } - - function openCreate() { - document.getElementById('c-lang').value = 'python'; - onLangChange(); - document.getElementById('c-timeout').value = '60'; - document.getElementById('c-schedule-enabled').checked = false; - document.getElementById('c-cron').value = ''; - toggleScheduleFields('c'); - document.getElementById('create-modal').classList.add('open'); - document.getElementById('c-name').focus(); - } - - function closeCreate() { - document.getElementById('create-modal').classList.remove('open'); - } - - async function submitCreate() { - const btn = document.getElementById('c-submit'); - btn.disabled = true; - const progress = startTimedStatus('Создаём функцию...', 'Создание функции...', explainDelay); - try { - const name = document.getElementById('c-name').value.trim(); - if (!name) throw new Error('name is required'); - const lang = document.getElementById('c-lang').value.trim(); - if (!lang) throw new Error('language is required'); - - await requestJSON(API_BASE + '/functions', 'POST', { - name: name, - language: lang, - entrypoint: document.getElementById('c-entry').value.trim(), - route: document.getElementById('c-route').value.trim(), - methods: parseMethods(document.getElementById('c-methods').value), - timeout: parseTimeout(document.getElementById('c-timeout').value), - code: document.getElementById('c-code').value - }); - - try { - await syncScheduleForFunction(name, 'c'); - } catch (scheduleErr) { - try { - await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE'); - } catch (rollbackErr) { - scheduleErr.message += '; rollback failed: ' + rollbackErr.message; - } - throw scheduleErr; - } - - closeCreate(); - progress.stop('\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0430: ' + name, 'ok'); - await reloadAll(); - } catch (e) { - progress.stop('\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f: ' + e.message, 'err'); - } finally { - btn.disabled = false; - } - } - - async function openEdit(name) { - try { - const fn = await getJSON(API_BASE + '/functions/' + encodeURIComponent(name)); - S.currentEdit = fn; - document.getElementById('e-title').textContent = '\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435: ' + name; - document.getElementById('e-name').value = name; - document.getElementById('e-env').value = fn.environment || ''; - document.getElementById('e-entry').value = fn.entrypoint || ''; - document.getElementById('e-timeout').value = String(fn.timeout || 60); - document.getElementById('e-code').value = fn.code || ''; - var schedule = timeTriggerByFn(name); - document.getElementById('e-schedule-enabled').checked = !!schedule; - document.getElementById('e-cron').value = (schedule && schedule.spec && schedule.spec.cron) || ''; - toggleScheduleFields('e'); - // Определяем язык по имени environment для линтера - var envName = (fn.environment || '').toLowerCase(); - var lang = 'python'; - if (envName.includes('node')) lang = 'nodejs'; - else if (envName.includes('go')) lang = 'go'; - else if (envName.includes('ruby')) lang = 'ruby'; - else if (envName.includes('php')) lang = 'php'; - document.getElementById('e-lang-hidden').value = lang; - // сбросить предыдущий AI-результат - var aiRes = document.getElementById('e-ai-result'); - if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; } - var warnEl = document.getElementById('e-tf-warn'); - if (warnEl) { - var isTf = /^tf-/.test(name) || /go[-_]env/.test(fn.environment || ''); - warnEl.style.display = isTf ? 'block' : 'none'; - } - document.getElementById('edit-modal').classList.add('open'); - } catch (e) { - showStatus('\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0438: ' + e.message, 'err'); - } - } - - function closeEdit() { - document.getElementById('edit-modal').classList.remove('open'); - S.currentEdit = null; - } - - async function submitEdit() { - if (!S.currentEdit) return; - const btn = document.getElementById('e-submit'); - btn.disabled = true; - const progress = startTimedStatus('Сохраняем код...', 'Сохранение кода...', explainDelay); - try { - const name = S.currentEdit.name; - await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', { - code: document.getElementById('e-code').value, - timeout: parseTimeout(document.getElementById('e-timeout').value) - }); - - await syncScheduleForFunction(name, 'e'); - closeEdit(); - progress.stop('\u041a\u043e\u0434 \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d: ' + name, 'ok'); - await reloadAll(); - } catch (e) { - progress.stop('\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f: ' + e.message, 'err'); - } finally { - btn.disabled = false; - } - } - - 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), - 'Invoke URL: ' + (result.invoke_url || 'n/a') - ].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; - } - } - - async function removeFn(name) { - var tfWarn = (/^tf-/.test(name)) ? '\n\n\u26a0\ufe0f \u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f Terraform. \u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0451\u0442 \u043a \u0440\u0430\u0441\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 state!' : ''; - if (!confirm('\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e ' + name + '?' + tfWarn)) return; - var progress = startTimedStatus('\u0423\u0434\u0430\u043b\u044f\u0435\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u044e...', '\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438...', explainDelay); - try { - await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE'); - progress.stop('\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0430: ' + name, 'ok'); - await reloadAll(); - } catch (e) { - progress.stop('\u041e\u0448\u0438\u0431\u043a\u0430 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f: ' + e.message, 'err'); - } - } - async function reloadAll() { try { const [envs, pkgs, fns, http, times] = await Promise.all([ diff --git a/console/ui/js/functions.js b/console/ui/js/functions.js new file mode 100644 index 0000000..cc55407 --- /dev/null +++ b/console/ui/js/functions.js @@ -0,0 +1,152 @@ +/* functions.js — CRUD операции с функциями */ + +function parseMethods(v) { + const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean); + return items.length ? Array.from(new Set(items)) : ['GET']; +} + +function parseTimeout(v) { + var n = Number(String(v || '').trim()); + if (!Number.isFinite(n) || n <= 0) return 60; + return Math.round(n); +} + +function onLangChange() { + var lang = document.getElementById('c-lang').value; + var t = LANG_TEMPLATES[lang]; + if (t) { + document.getElementById('c-entry').value = t.entrypoint; + document.getElementById('c-code').value = t.code; + } +} + +function openCreate() { + document.getElementById('c-lang').value = 'python'; + onLangChange(); + document.getElementById('c-timeout').value = '60'; + document.getElementById('c-schedule-enabled').checked = false; + document.getElementById('c-cron').value = ''; + toggleScheduleFields('c'); + document.getElementById('create-modal').classList.add('open'); + document.getElementById('c-name').focus(); +} + +function closeCreate() { + document.getElementById('create-modal').classList.remove('open'); +} + +async function submitCreate() { + const btn = document.getElementById('c-submit'); + btn.disabled = true; + const progress = startTimedStatus('Создаём функцию...', 'Создание функции...', explainDelay); + try { + const name = document.getElementById('c-name').value.trim(); + if (!name) throw new Error('name is required'); + const lang = document.getElementById('c-lang').value.trim(); + if (!lang) throw new Error('language is required'); + + await requestJSON(API_BASE + '/functions', 'POST', { + name: name, + language: lang, + entrypoint: document.getElementById('c-entry').value.trim(), + route: document.getElementById('c-route').value.trim(), + methods: parseMethods(document.getElementById('c-methods').value), + timeout: parseTimeout(document.getElementById('c-timeout').value), + code: document.getElementById('c-code').value + }); + + try { + await syncScheduleForFunction(name, 'c'); + } catch (scheduleErr) { + try { + await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE'); + } catch (rollbackErr) { + scheduleErr.message += '; rollback failed: ' + rollbackErr.message; + } + throw scheduleErr; + } + + closeCreate(); + progress.stop('Функция создана: ' + name, 'ok'); + await reloadAll(); + } catch (e) { + progress.stop('Ошибка создания: ' + e.message, 'err'); + } finally { + btn.disabled = false; + } +} + +async function openEdit(name) { + try { + const fn = await getJSON(API_BASE + '/functions/' + encodeURIComponent(name)); + S.currentEdit = fn; + document.getElementById('e-title').textContent = 'Редактирование: ' + name; + document.getElementById('e-name').value = name; + document.getElementById('e-env').value = fn.environment || ''; + document.getElementById('e-entry').value = fn.entrypoint || ''; + document.getElementById('e-timeout').value = String(fn.timeout || 60); + document.getElementById('e-code').value = fn.code || ''; + var schedule = timeTriggerByFn(name); + document.getElementById('e-schedule-enabled').checked = !!schedule; + document.getElementById('e-cron').value = (schedule && schedule.spec && schedule.spec.cron) || ''; + toggleScheduleFields('e'); + var envName = (fn.environment || '').toLowerCase(); + var lang = 'python'; + if (envName.includes('node')) lang = 'nodejs'; + else if (envName.includes('go')) lang = 'go'; + else if (envName.includes('ruby')) lang = 'ruby'; + else if (envName.includes('php')) lang = 'php'; + document.getElementById('e-lang-hidden').value = lang; + var aiRes = document.getElementById('e-ai-result'); + if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; } + var warnEl = document.getElementById('e-tf-warn'); + if (warnEl) { + var isTf = /^tf-/.test(name) || /go[-_]env/.test(fn.environment || ''); + warnEl.style.display = isTf ? 'block' : 'none'; + } + document.getElementById('edit-modal').classList.add('open'); + } catch (e) { + showStatus('Ошибка загрузки функции: ' + e.message, 'err'); + } +} + +function closeEdit() { + document.getElementById('edit-modal').classList.remove('open'); + S.currentEdit = null; +} + +async function submitEdit() { + if (!S.currentEdit) return; + const btn = document.getElementById('e-submit'); + btn.disabled = true; + const progress = startTimedStatus('Сохраняем код...', 'Сохранение кода...', explainDelay); + try { + const name = S.currentEdit.name; + await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', { + code: document.getElementById('e-code').value, + timeout: parseTimeout(document.getElementById('e-timeout').value) + }); + + await syncScheduleForFunction(name, 'e'); + closeEdit(); + progress.stop('Код обновлён: ' + name, 'ok'); + await reloadAll(); + } catch (e) { + progress.stop('Ошибка обновления: ' + e.message, 'err'); + } finally { + btn.disabled = false; + } +} + +async function removeFn(name) { + var tfWarn = (/^tf-/.test(name)) ? '\n\n⚠️ Эта функция управляется Terraform. Удаление приведёт к рассинхронизации state!' : ''; + if (!confirm('Удалить функцию ' + name + '?' + tfWarn)) return; + var progress = startTimedStatus('Удаляем функцию...', 'Удаление функции...', explainDelay); + try { + await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE'); + progress.stop('Функция удалена: ' + name, 'ok'); + await reloadAll(); + } catch (e) { + progress.stop('Ошибка удаления: ' + e.message, 'err'); + } +}