Files
app-autotest/site/static/js/instances.js
T

176 lines
8.7 KiB
JavaScript

// instances.js — список инстансов, выбор сервиса, toggle операций
//
// Отвечает за:
// - Загрузку инстансов выбранного сервиса (GET /api/operations/{svcId})
// - Отображение списка инстансов с фильтром autotest-
// - Показ доступных операций для выбранного инстанса
// - Кнопки операций (modify, delete, suspend, resume, redeploy)
//
// Глобальные переменные (используются в operations.js и других модулях):
// svcInstances[] — текущий список инстансов
// selectedInst — UUID выбранного инстанса (или null)
// selectedOp — {opId, opName, svcId} текущая выбранная операция
// currentSvcId — ID текущего сервиса
// currentSvcName — имя текущего сервиса
// currentSvcShort — короткое имя (для генерации displayName)
// busy — флаг блокировки UI (пока идёт операция)
// AUTOTEST_PREFIX — "autotest-" (фильтр "наших" инстансов)
let svcInstances = [], selectedInst = null, selectedOp = null;
let currentSvcId = (window.APP && window.APP.firstServiceId) || 1;
let currentSvcName = '';
let currentSvcShort = '';
let busy = false;
const AUTOTEST_PREFIX = 'autotest-';
/**
* Выбрать сервис — загрузить его инстансы и показать в списке.
* Вызывается при клике на сервис в левой панели.
*
* @param {number} svcId — ID сервиса
*/
async function selectService(svcId) {
if (busy) return; // блокировка — идёт операция
// Сброс предыдущего состояния
currentSvcId = svcId;
selectedInst = null;
selectedOp = null;
stopPoll(); // остановить поллинг предыдущей операции
// Скрыть панель параметров и этапы
document.getElementById('params-area').style.display = 'none';
document.querySelectorAll('.stages-box').forEach(e => e.remove());
// Подсветка активного сервиса в списке
document.querySelectorAll('.svc-item').forEach(e => e.classList.remove('active'));
const svcEl = document.querySelector(`.svc-item[onclick*="${svcId}"]`);
if (svcEl) svcEl.classList.add('active');
try {
// GET /api/operations/{svcId} — возвращает операции + autotest-инстансы
const r = await fetch('/api/operations/' + svcId);
const d = await r.json();
svcInstances = d.instances || [];
currentSvcName = d.svc || '';
currentSvcShort = d.svcShort || '';
// Обновить текст кнопки «Создать»
const btnSpan = document.querySelector('#create-btn-area span');
if (btnSpan) btnSpan.textContent = '+ Создать новый инстанс ' + (currentSvcName || 'сервиса');
renderInstances(); // отрисовать список
} catch (e) {
document.getElementById('inst-list').innerHTML =
'<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';
}
}
/**
* Отрисовать список инстансов в #inst-list.
* Каждый инстанс — строка: имя + сервис + статус + created + modified.
*/
function renderInstances() {
let html = '';
svcInstances.forEach(i => {
// Статус: из нашего поля status (cloud-first) или explainedStatus
const status = i.status || i.explainedStatus || '?';
const sc = (status === 'running' || status === 'Выполнен') ? 'badge-success' : '';
const created = i.instanceConfigDtCreated || i.dtCreate || '';
// Строка инстанса — кликабельна
html += `<div class="inst-item" onclick="toggleInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
<span style="display:inline-block;width:220px;">${_esc(i.displayName)}</span>
<span style="display:inline-block;width:140px;color:var(--muted);font-size:11px;">${_esc(i.svc || '')}</span>
<span class="badge ${sc}" style="font-size:9px;">${_esc(status)}</span>
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${created}">${relativeTime(created)}</span>
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtModified || ''}">${relativeTime(i.instanceConfigDtModified)}</span>
</div>`;
// Контейнер для кнопок операций (изначально скрыт)
html += `<div class="inst-ops" id="ops-${i.instanceUid}"></div>`;
});
document.getElementById('inst-list').innerHTML = html;
}
/**
* Обновить список инстансов (без смены сервиса).
* Вызывается после завершения операции.
*/
async function refreshInstances() {
try {
const r = await fetch('/api/operations/' + currentSvcId);
const d = await r.json();
svcInstances = d.instances || [];
renderInstances();
} catch (e) { }
}
/**
* Раскрыть/скрыть кнопки операций для инстанса.
* Загружает доступные операции через GET /api/operations/{svcId}.
*
* @param {string} iuid — instanceUid
*/
async function toggleInstance(iuid) {
if (busy) return;
selectedInst = iuid;
stopPoll();
// Подсветка выбранного инстанса
document.querySelectorAll('[data-iuid]').forEach(e =>
e.classList.toggle('active', e.dataset.iuid === iuid));
// Скрыть панель параметров (если была открыта для другого инстанса)
document.getElementById('params-area').style.display = 'none';
const opsEl = document.getElementById('ops-' + iuid);
// Если уже открыт — закрыть (toggle)
if (opsEl.classList.contains('open')) { opsEl.classList.remove('open'); return; }
// Закрыть ВСЕ другие открытые панели операций
document.querySelectorAll('.inst-ops').forEach(e => e.classList.remove('open'));
// Найти инстанс в списке (может быть из другого сервиса)
const inst = svcInstances.find(x => x.instanceUid === iuid);
const svcId = inst ? inst.serviceId : currentSvcId;
try {
const r = await fetch('/api/operations/' + svcId);
const d = await r.json();
// Фильтруем операции: убираем reconcile (техническая) и create (уже создан)
let ops = (d.operations || []).filter(o => o.operation !== 'reconcile' && o.operation !== 'create');
// Для not created инстансов — только delete
if (inst && (inst.status || inst.explainedStatus) === 'not created') {
ops = ops.filter(o => o.operation === 'delete');
}
// Кнопки операций с цветовой маркировкой
// o.operation в onclick — JS-escape: \ → \\, ' → \'
// o.operation в тексте кнопки — HTML-escape через _esc()
opsEl.innerHTML = ops.map(o => {
const escOp = _esc(o.operation);
const jsOp = o.operation.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
return `<button class="btn btn-sm op-btn ${opClass(o.operation)}" onclick="runOp('${jsOp}',${o.svcOperationId})">${escOp}</button>`;
}).join('');
opsEl.classList.add('open');
} catch (e) {
opsEl.innerHTML = '<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';
opsEl.classList.add('open');
}
}
/**
* CSS-класс для кнопки операции (цветовая маркировка).
*
* @param {string} opName — "modify"/"delete"/"suspend"/...
* @returns {string} CSS-класс
*/
function opClass(opName) {
if (opName === 'modify') return 'op-btn-modify'; // синий
if (opName === 'suspend') return 'op-btn-suspend'; // оранжевый
if (opName === 'resume') return 'op-btn-resume'; // зелёный
if (opName === 'delete') return 'op-btn-delete'; // красный
if (opName === 'redeploy') return 'op-btn-redeploy'; // фиолетовый
return '';
}