Files
sless/services/funcs/index.html
T

1147 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>sless console</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
}
a {
color: inherit;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #161b22;
}
::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 4px;
}
header {
padding: 14px 24px;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
gap: 12px;
background: #161b22;
position: sticky;
top: 0;
z-index: 10;
}
.logo {
font-weight: 700;
font-size: 1.1rem;
color: #58a6ff;
letter-spacing: -0.3px;
}
.logo-slash {
color: #484f58;
margin: 0 4px;
}
.ns-badge {
background: #1c2128;
border: 1px solid #30363d;
padding: 3px 12px;
border-radius: 20px;
font-family: monospace;
font-size: 0.82rem;
color: #8b949e;
}
.refresh-btn {
margin-left: auto;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
transition: background 0.1s;
}
.refresh-btn:hover {
background: #30363d;
}
main {
padding: 24px;
max-width: 960px;
margin: 0 auto;
}
.summary {
color: #8b949e;
font-size: 0.82rem;
margin-bottom: 18px;
}
.fn-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
margin-bottom: 10px;
overflow: hidden;
transition: border-color 0.15s;
}
.fn-card:hover {
border-color: #484f58;
}
.fn-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
cursor: pointer;
user-select: none;
flex-wrap: wrap;
row-gap: 6px;
}
.fn-header:hover {
background: rgba(255, 255, 255, 0.03);
}
.fn-name {
font-weight: 600;
font-size: 0.92rem;
color: #e6edf3;
}
.badge {
display: inline-block;
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.badge-runtime {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
}
.badge-Ready {
background: #1a4731;
border: 1px solid #1f6132;
color: #3fb950;
}
.badge-Building {
background: #382a0a;
border: 1px solid #5a4212;
color: #d29922;
}
.badge-Pending {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
}
.badge-Failed {
background: #3d1a1a;
border: 1px solid #6b1a1a;
color: #f85149;
}
.badge-kind-service {
background: #0d2538;
border: 1px solid #1158a7;
color: #58a6ff;
}
.badge-kind-function {
background: #1a2538;
border: 1px solid #30363d;
color: #8b949e;
}
.fn-url-row {
padding: 0 14px 9px 14px;
display: flex;
align-items: center;
gap: 6px;
}
.fn-url-row:empty {
display: none;
}
.fn-url {
font-size: 0.76rem;
color: #58a6ff;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.fn-url:hover {
text-decoration: underline;
}
.fn-cron {
font-size: 0.76rem;
color: #8b949e;
font-family: monospace;
}
.fn-msg {
font-size: 0.73rem;
color: #f85149;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.spacer {
flex: 1 0 0;
min-width: 4px;
}
.trigger-btn {
flex-shrink: 0;
padding: 4px 12px;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
font-size: 0.76rem;
font-weight: 500;
line-height: 1.5;
transition: opacity 0.1s, background 0.1s;
}
.trigger-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.trigger-btn.state-on {
background: #21262d;
border-color: #1f6132;
color: #3fb950;
}
.trigger-btn.state-on:hover:not(:disabled) {
background: #1a4731;
}
.trigger-btn.state-off {
background: #21262d;
border-color: #6b1a1a;
color: #f85149;
}
.trigger-btn.state-off:hover:not(:disabled) {
background: #3d1a1a;
}
.expand-icon {
color: #8b949e;
flex-shrink: 0;
font-size: 0.65rem;
transition: transform 0.18s;
}
.fn-body {
border-top: 1px solid #30363d;
}
.src-msg {
padding: 14px 16px;
color: #8b949e;
font-size: 0.85rem;
font-style: italic;
}
.file-block {
border-bottom: 1px solid #21262d;
}
.file-block:last-child {
border-bottom: none;
}
.file-header {
padding: 6px 16px;
background: #0d1117;
font-family: monospace;
font-size: 0.76rem;
color: #8b949e;
border-bottom: 1px solid #21262d;
}
.file-block pre {
margin: 0;
}
.file-block pre code.hljs {
border-radius: 0 !important;
font-size: 0.78rem !important;
padding: 14px 16px !important;
line-height: 1.5 !important;
}
.empty-state {
text-align: center;
padding: 64px 24px;
color: #484f58;
font-size: 0.9rem;
}
/* Кнопки действий на карточке функции */
.action-btn {
flex-shrink: 0;
padding: 3px 10px;
border-radius: 5px;
border: 1px solid #30363d;
cursor: pointer;
font-size: 0.73rem;
font-weight: 500;
background: #21262d;
color: #8b949e;
transition: background 0.1s, color 0.1s;
}
.action-btn:hover { background: #30363d; color: #c9d1d9; }
.action-btn.del:hover { background: #3d1a1a; border-color: #6b1a1a; color: #f85149; }
/* Кодовый редактор во встроенном режиме */
.edit-area {
width: 100%;
min-height: 220px;
background: #0d1117;
color: #e6edf3;
border: none;
border-top: 1px solid #30363d;
padding: 14px 16px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.78rem;
line-height: 1.55;
resize: vertical;
outline: none;
display: block;
box-sizing: border-box;
}
.edit-actions {
display: flex;
gap: 8px;
padding: 8px 14px;
background: #161b22;
border-top: 1px solid #30363d;
}
.edit-save-btn {
padding: 5px 16px;
background: #238636;
border: 1px solid #2ea043;
color: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.edit-save-btn:hover { background: #2ea043; }
.edit-save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.edit-cancel-btn {
padding: 5px 16px;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.edit-status {
font-size: 0.77rem;
padding: 5px 8px;
align-self: center;
color: #8b949e;
}
.edit-status.ok { color: #3fb950; }
.edit-status.err { color: #f85149; }
/* ---------- Модалка создания функции ---------- */
.modal-backdrop {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.65);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal-backdrop.open { display: flex; }
.modal {
background: #161b22;
border: 1px solid #30363d;
border-radius: 10px;
width: min(660px, 95vw);
max-height: 90vh;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.modal h2 {
font-size: 1rem;
font-weight: 600;
color: #e6edf3;
}
.modal label {
font-size: 0.82rem;
color: #8b949e;
display: block;
margin-bottom: 4px;
}
.modal input, .modal select {
width: 100%;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
padding: 7px 10px;
font-size: 0.85rem;
box-sizing: border-box;
outline: none;
}
.modal input:focus, .modal select:focus { border-color: #58a6ff; }
.modal-row { display: flex; gap: 12px; }
.modal-row > div { flex: 1; }
.modal-code {
width: 100%;
min-height: 200px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
padding: 10px 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.78rem;
line-height: 1.55;
resize: vertical;
outline: none;
box-sizing: border-box;
}
.modal-code:focus { border-color: #58a6ff; }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.modal-create-btn {
padding: 7px 20px;
background: #238636;
border: 1px solid #2ea043;
color: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.modal-create-btn:hover { background: #2ea043; }
.modal-create-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.modal-cancel-btn {
padding: 7px 20px;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.modal-err {
color: #f85149;
font-size: 0.8rem;
min-height: 18px;
}
.create-fn-btn {
margin-left: auto;
background: #238636;
border: 1px solid #2ea043;
color: #fff;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
transition: background 0.1s;
}
.create-fn-btn:hover { background: #2ea043; }
/* Таймер Building */
.fn-timer {
font-size: 0.72rem;
color: #d29922;
font-family: monospace;
margin-left: 2px;
flex-shrink: 0;
}
.badge-Building {
animation: pulse-bld 1.4s ease-in-out infinite;
}
@keyframes pulse-bld {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<header>
<span class="logo">sless</span>
<span class="logo-slash">/</span>
<span class="ns-badge" id="ns-label"></span>
<button class="refresh-btn" onclick="location.reload()">↻ обновить</button>
<button class="create-fn-btn" id="open-create-modal">+ Создать функцию</button>
</header>
<main>
<p class="summary" id="summary"></p>
<div id="fn-list"></div>
</main>
<!-- Модалка создания новой функции -->
<div class="modal-backdrop" id="create-modal">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Создать функцию</h2>
<div>
<label>Runtime</label>
<select id="m-runtime">
<option value="python3.11">Python 3.11</option>
<option value="nodejs20">Node.js 20</option>
</select>
</div>
<div class="modal-row">
<div>
<label>Имя функции (k8s name)</label>
<input id="m-name" type="text" placeholder="my-hello-fn" pattern="[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]">
</div>
<div>
<label>Entrypoint (module.func)</label>
<input id="m-entrypoint" type="text" value="handler.handler">
</div>
</div>
<div>
<label id="m-filename-label">Файл: <code id="m-filename-display">handler.py</code></label>
<textarea class="modal-code" id="m-code" spellcheck="false"></textarea>
</div>
<div>
<label><span id="m-deps-name">requirements.txt</span> <span style="color:#484f58;font-size:0.75rem">(зависимости, опционально)</span></label>
<textarea class="modal-code" id="m-deps" spellcheck="false" style="min-height:80px"></textarea>
</div>
<div class="modal-err" id="m-err"></div>
<div class="modal-actions">
<button class="modal-cancel-btn" id="modal-cancel">Отмена</button>
<button class="modal-create-btn" id="modal-submit">Создать</button>
</div>
</div>
</div>
<script id="fndata" type="application/json">__PAGE_DATA__</script>
<script>
(function () {
'use strict';
var raw = document.getElementById('fndata').textContent;
var data = JSON.parse(raw);
var ns = data.namespace;
var extURL = data.externalURL || '';
var fns = data.functions || [];
var byFn = data.triggersByFn || {};
document.getElementById('ns-label').textContent = ns;
var svcsCount = fns.filter(function(f){ return f.kind === 'service'; }).length;
var fnsCount = fns.filter(function(f){ return f.kind !== 'service'; }).length;
document.getElementById('summary').textContent =
'Функций: ' + fnsCount + ' | Сервисов: ' + svcsCount + ' | Всего: ' + fns.length;
var list = document.getElementById('fn-list');
if (fns.length === 0) {
var empty = $el('div', 'empty-state');
empty.textContent = 'Нет функций в этом namespace';
list.appendChild(empty);
}
for (var i = 0; i < fns.length; i++) {
list.appendChild(buildCard(fns[i], byFn[fns[i].name] || []));
}
// ---------- Карточки функций ----------
function buildCard(fn, triggers) {
var card = $el('div', 'fn-card');
var hdr = $el('div', 'fn-header');
var body = $el('div', 'fn-body');
body.style.display = 'none';
$txt(hdr, 'span', 'fn-name', fn.name);
$txt(hdr, 'span', 'badge badge-runtime', fn.runtime);
var phaseBadge = $txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase || 'Pending');
if (fn.kind) {
var kindLabel = fn.kind === 'service' ? 'always-on' : 'job';
$txt(hdr, 'span', 'badge badge-kind-' + fn.kind, kindLabel);
}
// Для Building/Pending — таймер секунд + автополинг до Ready/Failed
if (fn.phase === 'Building' || fn.phase === 'Pending') {
var timerEl = $txt(hdr, 'span', 'fn-timer', '0с');
startBuildPoll(phaseBadge, timerEl, fn, ns, null, function(url) {
if (!fn.url) { fn.url = url; setCardUrl(url); }
});
}
if (fn.message) $txt(hdr, 'span', 'fn-msg', fn.message);
hdr.appendChild($el('span', 'spacer'));
// urlRow — отдельная строка под заголовком для URL
var urlRow = $el('div', 'fn-url-row');
var cardUrlAnchor = null;
function setCardUrl(url) {
if (cardUrlAnchor) return; // уже есть
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(); });
urlRow.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) setCardUrl(url);
}
hdr.appendChild(makeTriggerBtn(tr, ns));
} else if (tr.type === 'cron') {
$txt(hdr, 'span', 'fn-cron', 'cron: ' + tr.schedule);
hdr.appendChild(makeTriggerBtn(tr, ns));
}
}
// Кнопка редактирования кода
var editBtn = $el('button', 'action-btn');
editBtn.textContent = '✎';
editBtn.title = 'Редактировать код';
editBtn.addEventListener('click', function (e) {
e.stopPropagation();
body.innerHTML = ''; // сбрасываем содержимое перед переходом в edit
loaded = false; // следующий expand снова зачитает исходники
body.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
openEditMode(body, ns, fn.name, fn.kind, fn.runtime);
});
hdr.appendChild(editBtn);
// Кнопка удаления
var delBtn = $el('button', 'action-btn del');
delBtn.textContent = '✕';
delBtn.title = 'Удалить функцию';
delBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (!confirm('Удалить функцию «' + fn.name + '»?\n\nЭто действие необратимо.')) return;
delBtn.disabled = true;
fetch('/funcs/' + ns + '/delete-function/' + fn.name + '?kind=' + (fn.kind || 'function'), {
method: 'DELETE'
}).then(function (r) {
if (r.ok) {
card.remove();
} else {
return r.text().then(function (t) { alert('Ошибка удаления: ' + t); });
}
}).catch(function (err) {
alert('Сетевая ошибка: ' + err);
}).finally(function () {
delBtn.disabled = false;
});
});
hdr.appendChild(delBtn);
var icon = $txt(hdr, 'span', 'expand-icon', '▶');
var loaded = false;
hdr.addEventListener('click', function () {
if (body.style.display === 'none') {
body.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
if (!loaded) {
loaded = true;
loadSource(body, ns, fn.name, fn.kind);
}
} else {
body.style.display = 'none';
icon.style.transform = '';
}
});
card.appendChild(hdr);
card.appendChild(urlRow);
card.appendChild(body);
return card;
}
// openEditMode — открывает тело карточки в режиме редактирования.
// Загружает текущий код и файл зависимостей (requirements.txt / package.json).
function openEditMode(body, ns, fnName, fnKind, fnRuntime) {
body.innerHTML = '';
var msg = $el('div', 'src-msg');
msg.textContent = 'Загрузка кода…';
body.appendChild(msg);
var kindParam = fnKind === 'service' ? '?kind=service' : '';
fetch('/funcs/' + ns + '/source/' + fnName + kindParam)
.then(function (r) {
body.removeChild(msg);
if (!r.ok) {
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка загрузки кода: ' + r.status;
body.appendChild(e);
return;
}
return r.json().then(function (files) {
var editable = files.filter(function (f) {
return !f.binary && (f.name.endsWith('.py') || f.name.endsWith('.js'));
});
if (editable.length === 0) {
editable = [{ name: defaultFilename(fnRuntime), content: helloWorldTemplate(fnRuntime) }];
}
var mainFile = editable[0];
// Ищем файл зависимостей (requirements.txt / package.json)
var tpl = TEMPLATES[fnRuntime] || {};
var depsFilename = tpl.depsFilename || '';
var depsContent = tpl.deps || '';
if (depsFilename) {
for (var i = 0; i < files.length; i++) {
if (files[i].name === depsFilename && !files[i].binary) {
depsContent = files[i].content;
break;
}
}
}
renderEditArea(body, ns, fnName, fnKind, fnRuntime,
mainFile.name, mainFile.content, depsFilename, depsContent);
});
})
.catch(function (err) {
body.innerHTML = '';
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка: ' + err;
body.appendChild(e);
});
}
// renderEditArea — rename input + code textarea + deps textarea + Save/Cancel.
function renderEditArea(body, ns, fnName, fnKind, fnRuntime, filename, initialCode, depsFilename, depsContent) {
body.innerHTML = '';
// Строка переименования
var renameRow = $el('div', 'edit-actions');
renameRow.style.borderBottom = '1px solid #30363d';
var nameLabel = $el('span', 'edit-status');
nameLabel.textContent = 'Имя:';
var nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = fnName;
nameInput.style.cssText = 'flex:1;max-width:240px;background:#0d1117;border:1px solid #30363d;border-radius:5px;color:#e6edf3;padding:4px 8px;font-size:0.8rem;outline:none;';
var renameBtn = $el('button', 'edit-cancel-btn');
renameBtn.textContent = 'Переименовать';
var renameStatus = $el('span', 'edit-status');
renameRow.appendChild(nameLabel);
renameRow.appendChild(nameInput);
renameRow.appendChild(renameBtn);
renameRow.appendChild(renameStatus);
body.appendChild(renameRow);
// Код функции
var codeHdr = $el('div', 'file-header');
codeHdr.textContent = filename;
body.appendChild(codeHdr);
var ta = document.createElement('textarea');
ta.className = 'edit-area';
ta.spellcheck = false;
ta.value = initialCode;
body.appendChild(ta);
// Файл зависимостей (если есть)
var depsTA = null;
if (depsFilename) {
var depsHdr = $el('div', 'file-header');
depsHdr.textContent = depsFilename;
body.appendChild(depsHdr);
depsTA = document.createElement('textarea');
depsTA.className = 'edit-area';
depsTA.style.minHeight = '80px';
depsTA.spellcheck = false;
depsTA.value = depsContent;
body.appendChild(depsTA);
}
// Кнопки Save / Cancel
var actions = $el('div', 'edit-actions');
var saveBtn = $el('button', 'edit-save-btn');
saveBtn.textContent = 'Сохранить';
var cancelBtn = $el('button', 'edit-cancel-btn');
cancelBtn.textContent = 'Закрыть';
var status = $el('span', 'edit-status');
actions.appendChild(saveBtn);
actions.appendChild(cancelBtn);
actions.appendChild(status);
body.appendChild(actions);
saveBtn.addEventListener('click', function () {
saveBtn.disabled = true;
status.textContent = 'Сохраняю…';
status.className = 'edit-status';
fetch('/funcs/' + ns + '/save-function/' + fnName, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: ta.value, filename: filename, kind: fnKind || 'function',
deps: depsTA ? depsTA.value : '',
deps_filename: depsFilename || ''
})
}).then(function (r) {
saveBtn.disabled = false;
if (r.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;
status.className = 'edit-status err';
});
}
}).catch(function (err) {
saveBtn.disabled = false;
status.textContent = '✗ ' + err;
status.className = 'edit-status err';
});
});
cancelBtn.addEventListener('click', function () { body.style.display = 'none'; });
renameBtn.addEventListener('click', function () {
var newName = nameInput.value.trim();
if (newName === fnName) { renameStatus.textContent = 'Имя не изменилось'; return; }
if (!/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/.test(newName)) {
renameStatus.textContent = '✗ Только строчные буквы, цифры, дефисы';
renameStatus.className = 'edit-status err';
return;
}
renameBtn.disabled = true;
renameStatus.textContent = 'Переименовываю…';
renameStatus.className = 'edit-status';
fetch('/funcs/' + ns + '/rename-function/' + fnName, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ new_name: newName, kind: fnKind || 'function' })
}).then(function (r) {
if (r.ok) {
renameStatus.textContent = '✓ Переименовано → ' + newName;
renameStatus.className = 'edit-status ok';
setTimeout(function () { location.reload(); }, 800);
} else {
return r.text().then(function (t) {
renameStatus.textContent = '✗ ' + t;
renameStatus.className = 'edit-status err';
});
}
}).catch(function (err) {
renameStatus.textContent = '✗ ' + err;
renameStatus.className = 'edit-status err';
}).finally(function () { renameBtn.disabled = false; });
});
}
// startBuildPoll — опрашивает /fn-status каждые 5с пока phase=Building/Pending.
// Показывает таймер-строку в statusEl (если передан), обновляет phaseBadge,
// добавляет URL через onUrl-коллбэк когда сервис появляется.
function startBuildPoll(phaseBadge, timerEl, fn, ns, statusEl, onUrl) {
var buildStart = Date.now();
var timerItv = setInterval(function () {
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'))
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.phase || d.phase === 'Building' || d.phase === 'Pending') return;
clearInterval(timerItv);
clearInterval(pollItv);
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 () {});
}, 5000);
}
function makeTriggerBtn(tr, ns) {
var btn = $el('button', 'trigger-btn ' + (tr.enabled ? 'state-on' : 'state-off'));
btn.textContent = tr.enabled ? '■ стоп' : '▶ запуск';
btn.title = tr.name;
btn.addEventListener('click', function (e) {
e.stopPropagation();
btn.disabled = true;
var newVal = !tr.enabled;
fetch('/funcs/' + ns + '/triggers/' + tr.name, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: newVal })
}).then(function (r) {
if (r.ok) {
tr.enabled = newVal;
btn.className = 'trigger-btn ' + (newVal ? 'state-on' : 'state-off');
btn.textContent = newVal ? '■ стоп' : '▶ запуск';
} else {
return r.text().then(function (t) { alert('Ошибка: ' + t); });
}
}).catch(function (err) {
alert('Сетевая ошибка: ' + err);
}).finally(function () {
btn.disabled = false;
});
});
return btn;
}
function loadSource(body, ns, fnName, fnKind) {
body.innerHTML = ''; // всегда очищаем перед загрузкой
var msg = $el('div', 'src-msg');
msg.textContent = 'Загрузка кода…';
body.appendChild(msg);
var kindParam = fnKind === 'service' ? '?kind=service' : '';
fetch('/funcs/' + ns + '/source/' + fnName + kindParam)
.then(function (r) {
body.removeChild(msg);
if (!r.ok) {
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка ' + r.status;
body.appendChild(e);
return;
}
return r.json().then(function (files) {
var shown = files.filter(function (f) { return !f.binary; });
if (shown.length === 0) {
var e = $el('div', 'src-msg');
e.textContent = 'Код не загружен';
body.appendChild(e);
return;
}
for (var i = 0; i < shown.length; i++) {
var f = shown[i];
var block = $el('div', 'file-block');
var fhdr = $el('div', 'file-header');
fhdr.textContent = f.name;
var pre = document.createElement('pre');
var code = document.createElement('code');
code.className = 'language-' + detectLang(f.name);
code.textContent = f.content;
pre.appendChild(code);
block.appendChild(fhdr);
block.appendChild(pre);
body.appendChild(block);
}
hljs.highlightAll();
});
})
.catch(function (err) {
body.innerHTML = '';
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка: ' + err;
body.appendChild(e);
});
}
// ---------- Модалка создания функции ----------
var TEMPLATES = {
'python3.11': {
filename: 'handler.py',
code: '# Hello World — Python 3.11\n# Entrypoint: handler.handler\n\ndef handler(event):\n name = event.get("name", "World")\n return {"result": f"Hello, {name}!"}\n',
depsFilename: 'requirements.txt',
deps: '# Python packages\n# e.g.: requests==2.31.0\n'
},
'nodejs20': {
filename: 'handler.js',
code: '// Hello World — Node.js 20\n// Entrypoint: handler.handler\n\nexports.handler = async (event) => {\n const name = event.name || \'World\';\n return { result: `Hello, ${name}!` };\n};\n',
depsFilename: 'package.json',
deps: '{\n "dependencies": {}\n}\n'
}
};
function defaultFilename(runtime) {
return TEMPLATES[runtime] ? TEMPLATES[runtime].filename : 'handler.py';
}
function helloWorldTemplate(runtime) {
return TEMPLATES[runtime] ? TEMPLATES[runtime].code : '# код функции\n';
}
var modal = document.getElementById('create-modal');
var mRuntime = document.getElementById('m-runtime');
var mCode = document.getElementById('m-code');
var mFilenameDisplay = document.getElementById('m-filename-display');
var mErr = document.getElementById('m-err');
// Заполняем шаблон при открытии / смене runtime
function applyTemplate() {
var tpl = TEMPLATES[mRuntime.value];
if (tpl) {
mCode.value = tpl.code;
mFilenameDisplay.textContent = tpl.filename;
document.getElementById('m-deps-name').textContent = tpl.depsFilename;
document.getElementById('m-deps').value = tpl.deps || '';
}
}
applyTemplate();
mRuntime.addEventListener('change', applyTemplate);
document.getElementById('open-create-modal').addEventListener('click', function () {
applyTemplate();
mErr.textContent = '';
document.getElementById('m-name').value = '';
document.getElementById('m-entrypoint').value = 'handler.handler';
modal.classList.add('open');
document.getElementById('m-name').focus();
});
document.getElementById('modal-cancel').addEventListener('click', function () {
modal.classList.remove('open');
});
modal.addEventListener('click', function (e) {
if (e.target === modal) modal.classList.remove('open');
});
document.getElementById('modal-submit').addEventListener('click', function () {
var name = document.getElementById('m-name').value.trim();
var runtime = mRuntime.value;
var entrypoint = document.getElementById('m-entrypoint').value.trim();
var code = mCode.value;
var filename = TEMPLATES[runtime] ? TEMPLATES[runtime].filename : 'handler.py';
mErr.textContent = '';
if (!name) { mErr.textContent = 'Укажите имя функции'; return; }
if (!/^[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) {
mErr.textContent = 'Имя должно быть в формате k8s: строчные буквы, цифры, дефисы';
return;
}
if (!entrypoint) { mErr.textContent = 'Укажите entrypoint (например handler.handler)'; return; }
if (!code.trim()) { mErr.textContent = 'Напишите код функции'; return; }
var btn = document.getElementById('modal-submit');
btn.disabled = true;
mErr.textContent = 'Создаю…';
fetch('/funcs/' + ns + '/create-function', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
runtime: runtime,
kind: 'service',
code: code,
filename: filename,
entrypoint: entrypoint,
memory_mb: 128,
timeout_sec: 30,
deps: document.getElementById('m-deps').value,
deps_filename: TEMPLATES[runtime] ? TEMPLATES[runtime].depsFilename : ''
})
}).then(function (r) {
if (r.ok) {
modal.classList.remove('open');
mErr.textContent = '';
// Перезагружаем страницу чтобы увидеть новую функцию
setTimeout(function () { location.reload(); }, 400);
} else {
return r.text().then(function (t) {
mErr.textContent = '✗ ' + t;
});
}
}).catch(function (err) {
mErr.textContent = '✗ Сетевая ошибка: ' + err;
}).finally(function () {
btn.disabled = false;
});
});
// ---------- Утилиты ----------
function detectLang(name) {
var ext = name.split('.').pop().toLowerCase();
var m = {
py: 'python', js: 'javascript', ts: 'typescript', go: 'go',
sh: 'bash', bash: 'bash', yml: 'yaml', yaml: 'yaml',
json: 'json', md: 'markdown', txt: 'plaintext', toml: 'toml',
rs: 'rust', rb: 'ruby', java: 'java', cpp: 'cpp', c: 'c',
cs: 'csharp', php: 'php', html: 'html', css: 'css'
};
return m[ext] || 'plaintext';
}
function $el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
function $txt(p, tag, cls, text) { var e = $el(tag, cls); e.textContent = text; p.appendChild(e); return e; }
})();
</script>
</body>
</html>