v1.2.4: collapsible help section in scenario editor modal
scenario-form.js: <details> block between name and steps: - How steps work (operation → target) - Create: service + output name - Non-create: instance from scenario or cloud - Parameters: key=value format with example - Buttons: ↑↓ reorder, ✕ delete
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ from routes.api_scenario_run import bp_run as api_scenario_run_bp
|
||||
from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
||||
|
||||
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
||||
VERSION = "1.2.3"
|
||||
VERSION = "1.2.4"
|
||||
|
||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc")
|
||||
|
||||
@@ -55,6 +55,18 @@ function renderEditor() {
|
||||
html += '<div style="margin-bottom:10px;"><label style="font-size:12px;font-weight:600;">Название</label><br>';
|
||||
html += `<input type="text" id="scenario-name" value="${_esc(st.name)}" style="width:100%;padding:6px;font-size:13px;" placeholder="my_test">`;
|
||||
html += '</div>';
|
||||
// Справка
|
||||
html += `<details style="margin-bottom:10px;font-size:11px;color:var(--muted);border:1px solid var(--brand-gray);border-radius:4px;padding:6px 8px;">
|
||||
<summary style="cursor:pointer;font-weight:600;">📖 Как заполнять</summary>
|
||||
<div style="margin-top:6px;line-height:1.6;">
|
||||
<b>Шаг:</b> выбери <b>операцию</b> → укажи цель.<br>
|
||||
<b>Create:</b> выбери сервис + задай <b>output</b> (имя, например <code>d1</code>). Другие шаги смогут ссылаться на него.<br>
|
||||
<b>Остальные операции:</b> выбери инстанс — 🆕 из create-шагов этого сценария или ☁ из облака.<br>
|
||||
<b>Параметры:</b> key = value (символические коды, как в API).<br>
|
||||
Пример: <code>durationMs</code> = <code>5000</code><br>
|
||||
<b>[↑][↓]</b> — порядок шагов, <b>[✕]</b> — удалить шаг.
|
||||
</div>
|
||||
</details>`;
|
||||
// Шаги
|
||||
html += '<div style="font-size:12px;font-weight:600;margin-bottom:6px;">Шаги</div>';
|
||||
st.steps.forEach((s, idx) => { html += renderStepRow(idx, s); });
|
||||
@@ -69,25 +81,10 @@ function renderEditor() {
|
||||
|
||||
function renderStepRow(idx, step) {
|
||||
const st = scenarioEditorState;
|
||||
const svcId = step.service_id;
|
||||
const op = step.operation;
|
||||
const out = step.output || '';
|
||||
const ref = step.instance_ref || '';
|
||||
|
||||
// Сервисы — dropdown
|
||||
let svcOpts = '<option value="">— сервис —</option>';
|
||||
st.services.forEach(s => {
|
||||
svcOpts += `<option value="${s.svcId}" ${s.svcId == svcId ? 'selected' : ''}>${s.svcId}. ${_esc(s.svc)}</option>`;
|
||||
});
|
||||
|
||||
// Операции — dropdown (из кеша или базовый)
|
||||
let opOpts = '<option value="">— операция —</option>';
|
||||
const ops = st.opsCache[svcId] || ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy'];
|
||||
ops.forEach(o => {
|
||||
const oName = typeof o === 'string' ? o : o.operation;
|
||||
opOpts += `<option value="${oName}" ${oName == op ? 'selected' : ''}>${oName}</option>`;
|
||||
});
|
||||
|
||||
// Output refs из предыдущих create-шагов
|
||||
const prevOutputs = [];
|
||||
for (let i = 0; i < idx; i++) {
|
||||
@@ -103,31 +100,37 @@ function renderStepRow(idx, step) {
|
||||
html += `<button class="btn btn-sm" style="font-size:9px;padding:0 4px;margin-left:auto;color:var(--destructive);" onclick="removeStep(${idx})">✕</button>`;
|
||||
html += `</div>`;
|
||||
|
||||
// Сервис + Операция в одной строке
|
||||
html += `<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">`;
|
||||
html += `<select id="step-${idx}-svc" onchange="onSvcChange(${idx})" style="padding:4px;font-size:12px;">${svcOpts}</select>`;
|
||||
html += `<select id="step-${idx}-op" onchange="onOpChange(${idx})" style="padding:4px;font-size:12px;">${opOpts}</select>`;
|
||||
html += `</div>`;
|
||||
// 1. Операция ВСЕГДА первая
|
||||
const allOps = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy'];
|
||||
let opOpts = '<option value="">— операция —</option>';
|
||||
allOps.forEach(o => { opOpts += `<option value="${o}" ${o == op ? 'selected' : ''}>${o}</option>`; });
|
||||
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-op" onchange="onOpChange(${idx})" style="padding:4px;font-size:12px;">${opOpts}</select></div>`;
|
||||
|
||||
// Output / instance_ref / instance_uid
|
||||
html += '<div style="display:flex;gap:6px;align-items:center;margin-top:4px;flex-wrap:wrap;">';
|
||||
// 2. В зависимости от операции
|
||||
if (op === 'create') {
|
||||
html += `<input type="text" id="step-${idx}-output" value="${_esc(out)}" placeholder="output (имя для ссылок)" style="width:180px;padding:3px;font-size:11px;">`;
|
||||
} else if (op) {
|
||||
// Предыдущие output'ы + облачные инстансы этого сервиса
|
||||
let refOpts = '<option value="">— инстанс —</option>';
|
||||
// output'ы из предыдущих create-шагов
|
||||
prevOutputs.forEach(o => { refOpts += `<option value="${o}" ${o == ref ? 'selected' : ''}>🆕 ${_esc(o)}</option>`; });
|
||||
// Облачные инстансы того же сервиса
|
||||
const cloudInsts = (st.cloudInstances || []).filter(i => i.serviceId == svcId && i.explainedStatus !== 'deleted');
|
||||
if (cloudInsts.length) refOpts += '<option disabled>── облако ──</option>';
|
||||
cloudInsts.forEach(i => {
|
||||
const sel = i.instanceUid == ref ? 'selected' : '';
|
||||
refOpts += `<option value="${i.instanceUid}" ${sel}>☁ ${_esc(i.displayName)} (${_esc(i.explainedStatus || '?')})</option>`;
|
||||
// Сервис
|
||||
let svcOpts = '<option value="">— сервис —</option>';
|
||||
st.services.forEach(s => {
|
||||
svcOpts += `<option value="${s.svcId}" ${s.svcId == step.service_id ? 'selected' : ''}>${s.svcId}. ${_esc(s.svc)}</option>`;
|
||||
});
|
||||
html += `<select id="step-${idx}-ref" style="padding:3px;font-size:11px;max-width:400px;">${refOpts}</select>`;
|
||||
html += `<div style="display:flex;gap:6px;align-items:center;"><select id="step-${idx}-svc" style="padding:4px;font-size:12px;">${svcOpts}</select></div>`;
|
||||
html += `<div style="margin-top:4px;"><input type="text" id="step-${idx}-output" value="${_esc(out)}" placeholder="имя для ссылок (например: d1)" style="width:220px;padding:3px;font-size:11px;"></div>`;
|
||||
} else if (op) {
|
||||
// Инстанс — облачные + предыдущие output'ы
|
||||
let refOpts = '<option value="">— инстанс —</option>';
|
||||
// output'ы из create-шагов
|
||||
prevOutputs.forEach(o => { refOpts += `<option value="${o}" ${o == ref ? 'selected' : ''}>🆕 ${_esc(o)} (из шага)</option>`; });
|
||||
// Облачные инстансы
|
||||
if (st.cloudInstances && st.cloudInstances.length) {
|
||||
refOpts += '<option disabled>── облако ──</option>';
|
||||
st.cloudInstances.forEach(i => {
|
||||
if (i.explainedStatus === 'deleted') return;
|
||||
const sel = i.instanceUid == ref ? 'selected' : '';
|
||||
refOpts += `<option value="${i.instanceUid}" ${sel}>☁ ${_esc(i.displayName)} — ${_esc(i.svc||'')} (${_esc(i.explainedStatus||'?')})</option>`;
|
||||
});
|
||||
}
|
||||
html += `<div><select id="step-${idx}-ref" style="padding:4px;font-size:12px;max-width:500px;">${refOpts}</select></div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Параметры
|
||||
html += `<div style="margin-top:4px;" id="step-${idx}-params">`;
|
||||
@@ -145,24 +148,7 @@ function renderStepRow(idx, step) {
|
||||
return html;
|
||||
}
|
||||
|
||||
async function onSvcChange(idx) {
|
||||
const svcEl = document.getElementById('step-' + idx + '-svc');
|
||||
const svcId = parseInt(svcEl?.value) || 0;
|
||||
if (!svcId) return;
|
||||
// Загрузить операции для сервиса
|
||||
try {
|
||||
const r = await fetch('/api/operations/' + svcId);
|
||||
const d = await r.json();
|
||||
scenarioEditorState.opsCache[svcId] = d.operations || [];
|
||||
} catch (e) {
|
||||
scenarioEditorState.opsCache[svcId] = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy'];
|
||||
}
|
||||
renderEditor();
|
||||
}
|
||||
|
||||
async function onOpChange(idx) {
|
||||
// Ничего не делаем — параметры редактируются вручную
|
||||
}
|
||||
function onOpChange(idx) { renderEditor(); }
|
||||
|
||||
function addStep() {
|
||||
scenarioEditorState.steps.push({ service_id: '', operation: '', output: '', instance_ref: '', instance_uid: '', params: [] });
|
||||
@@ -194,26 +180,40 @@ function collectFormSteps() {
|
||||
const name = (document.getElementById('scenario-name')?.value || '').trim();
|
||||
const steps = [];
|
||||
st.steps.forEach((_, idx) => {
|
||||
const svcEl = document.getElementById('step-' + idx + '-svc');
|
||||
const opEl = document.getElementById('step-' + idx + '-op');
|
||||
const outEl = document.getElementById('step-' + idx + '-output');
|
||||
const refEl = document.getElementById('step-' + idx + '-ref');
|
||||
const uidEl = document.getElementById('step-' + idx + '-uid');
|
||||
const svcId = parseInt(svcEl?.value) || 0;
|
||||
const operation = (opEl?.value || '').trim();
|
||||
const output = (outEl?.value || '').trim();
|
||||
const instance_ref = (refEl?.value || '').trim();
|
||||
const instance_uid = (uidEl?.value || '').trim();
|
||||
const params = {};
|
||||
let svcId = 0;
|
||||
const s = { operation, params: {} };
|
||||
|
||||
if (operation === 'create') {
|
||||
const svcEl = document.getElementById('step-' + idx + '-svc');
|
||||
svcId = parseInt(svcEl?.value) || 0;
|
||||
const outEl = document.getElementById('step-' + idx + '-output');
|
||||
const output = (outEl?.value || '').trim();
|
||||
if (output) s.output = output;
|
||||
} else {
|
||||
const refEl = document.getElementById('step-' + idx + '-ref');
|
||||
const ref = (refEl?.value || '').trim();
|
||||
if (ref) {
|
||||
// Это UUID облачного инстанса — ищем его service_id
|
||||
const cloud = (st.cloudInstances || []).find(i => i.instanceUid === ref);
|
||||
if (cloud) {
|
||||
svcId = cloud.serviceId;
|
||||
s.instance_uid = ref;
|
||||
} else {
|
||||
// Это output-имя из предыдущего шага
|
||||
s.instance_ref = ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
s.service_id = svcId;
|
||||
|
||||
// Параметры
|
||||
st.steps[idx].params.forEach((__, pi) => {
|
||||
const k = document.getElementById('step-' + idx + '-pk-' + pi)?.value?.trim();
|
||||
const v = document.getElementById('step-' + idx + '-pv-' + pi)?.value || '';
|
||||
if (k) params[k] = v;
|
||||
if (k) s.params[k] = v;
|
||||
});
|
||||
const s = { service_id: svcId, operation, params };
|
||||
if (output) s.output = output;
|
||||
if (instance_ref) s.instance_ref = instance_ref;
|
||||
if (instance_uid) s.instance_uid = instance_uid;
|
||||
steps.push(s);
|
||||
});
|
||||
return { name, steps };
|
||||
|
||||
Reference in New Issue
Block a user