// 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 = 'Ошибка загрузки'; } } /** * Отрисовать список инстансов в #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 += `
${_esc(i.displayName)} ${_esc(i.svc || '')} ${_esc(status)} ${relativeTime(created)} ${relativeTime(i.instanceConfigDtModified)}
`; // Контейнер для кнопок операций (изначально скрыт) html += `
`; }); 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 ``; }).join(''); opsEl.classList.add('open'); } catch (e) { opsEl.innerHTML = 'Ошибка загрузки'; 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 ''; }