- operator: GET /v1/namespaces/{ns}/functions/{name}/source
reads build context tar.gz from S3, strips Dockerfile, returns JSON files
- funcs-service v0.2.0:
- Accept: text/html → dark-themed HTML console with accordion cards
- GET /funcs/{ns}/source/{fn} → proxy to operator (service token auth)
- PATCH /funcs/{ns}/triggers/{name} → proxy enable/disable (only enabled field)
- curl (no text/html Accept) → plain text as before (backward compat)
- highlight.js syntax highlighting per file extension
- operator v0.1.34, funcs-service v0.2.0
492 lines
15 KiB
HTML
492 lines
15 KiB
HTML
<!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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
</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>
|
|
</header>
|
|
<main>
|
|
<p class="summary" id="summary"></p>
|
|
<div id="fn-list"></div>
|
|
</main>
|
|
|
|
<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;
|
|
document.getElementById('summary').textContent = 'Функций: ' + 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);
|
|
$txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase);
|
|
|
|
for (var t = 0; t < triggers.length; t++) {
|
|
var tr = triggers[t];
|
|
if (tr.type === 'http') {
|
|
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);
|
|
}
|
|
hdr.appendChild(makeTriggerBtn(tr, ns));
|
|
} else if (tr.type === 'cron') {
|
|
$txt(hdr, 'span', 'fn-cron', 'cron: ' + tr.schedule);
|
|
hdr.appendChild(makeTriggerBtn(tr, ns));
|
|
}
|
|
}
|
|
|
|
if (fn.message) $txt(hdr, 'span', 'fn-msg', fn.message);
|
|
hdr.appendChild($el('span', 'spacer'));
|
|
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);
|
|
}
|
|
} else {
|
|
body.style.display = 'none';
|
|
icon.style.transform = '';
|
|
}
|
|
});
|
|
|
|
card.appendChild(hdr);
|
|
card.appendChild(body);
|
|
return card;
|
|
}
|
|
|
|
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) {
|
|
var msg = $el('div', 'src-msg');
|
|
msg.textContent = 'Загрузка кода…';
|
|
body.appendChild(msg);
|
|
|
|
fetch('/funcs/' + ns + '/source/' + fnName)
|
|
.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);
|
|
});
|
|
}
|
|
|
|
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> |