Files
cloud-ip-validator/internal/dashboard/static/analytics-compare.js
T

136 lines
10 KiB
JavaScript
Raw Normal View History

2026-10-04 10:26:37 +03:00
/* Comparison of two finished runs: renders the comparison embedded as JSON in
#analytics-data (the groups of addresses, the dynamics of the seven
indicators, the verdict transitions) and opens the address list behind each
number in the shared dialog (analytics-dialog.js). The state of the page is
its address: ?base=A&target=B. */
(function () {
'use strict';
var D = JSON.parse(document.getElementById('analytics-data').textContent);
var R = D.report, M = D.meta, G = R.groups, T = R.transitions;
var A = window.AnalyticsDialog, $ = A.$, fmt = A.fmt, esc = A.esc, tipAttr = A.tipAttr;
function pct(a, b) { return b ? Math.round(a / b * 100) : 0; }
var NAME = {};
R.indicators.forEach(function (d) { NAME[d.key] = d.name; });
/* ---- what each list says about itself ---- */
var GROUP = {
new: ['Новые адреса', 'Есть в запуске B, в A их не было: прибыли в проект и подключились к анализу. Столбцы показывают состояние в B.'],
left: ['Выбывшие адреса', 'Были в запуске A, в B их нет: вышли из состава проекта. Столбцы показывают состояние в A.'],
common: ['Общие адреса', 'Есть в обоих запусках. Столбцы показывают состояние в A → B.'],
changed: ['Изменившиеся адреса', 'Есть в обоих запусках, принадлежность хотя бы к одному из семи индикаторов в A и в B разная.'],
same: ['Адреса без изменений', 'Есть в обоих запусках, принадлежность ко всем индикаторам та же. Другой набор проваленных целей или площадок изменением не считается.'],
entered: ['Вошли в индикатор', 'Общие адреса, которые в A не входили в этот индикатор, а в B входят.'],
exited: ['Вышли из индикатора', 'Общие адреса, которые в A входили в этот индикатор, а в B не входят.']
};
var HINT_ONE = {
2: 'Вердикт адреса, который система выставила при агрегации, а не итог всех проверок.',
3: 'Сколько egress-проверок адреса успешно из всех записанных в последнем цикле запуска. «—» — egress-проверок нет.',
4: 'Сколько ingress-проверок адреса успешно из всех записанных в последнем цикле запуска. «—» — ingress-проверок нет.',
5: 'Индикаторы страницы «Аналитика», в которые входит адрес. Один адрес может входить в несколько; вердикт — ровно в один из трёх.'
};
var HINT_PAIR = {
2: 'Вердикт адреса в запуске A и в запуске B.',
3: 'Сколько egress-проверок адреса успешно из всех записанных в последнем цикле: в A → в B.',
4: 'Сколько ingress-проверок адреса успешно из всех записанных в последнем цикле: в A → в B.',
5: 'Что изменилось между запусками, по шагам: вердикт, вход в индикатор и выход из него, проваленные цели https и площадки ssh, валидатор. У адреса без изменений в индикаторах — «без изменений», даже если набор проваленных целей другой.'
};
function describe(f) {
var g = GROUP[f.group], ind = f.ind ? '«' + (NAME[f.ind] || f.ind) + '»' : '';
var title = g[0] + (f.ind ? ': ' + (NAME[f.ind] || f.ind) : '') + (f.from ? ' · вердикт ' + f.from + ' → ' + f.to : '');
var note = g[1];
if (f.ind && f.group === 'new') note += ' Только входящие в индикатор ' + ind + ' в запуске B.';
if (f.ind && f.group === 'left') note += ' Только входившие в индикатор ' + ind + ' в запуске A.';
if (f.ind && (f.group === 'common' || f.group === 'changed' || f.group === 'same')) note += ' Только входящие в индикатор ' + ind + ' хотя бы в одном из запусков.';
if (f.from) note += ' Только адреса с вердиктом ' + f.from + ' в A и ' + f.to + ' в B.';
return { title: title, note: note, hints: f.group === 'new' || f.group === 'left' ? HINT_ONE : HINT_PAIR };
}
function listURL(root, f) {
return root + encodeURIComponent(f.group) + '?base=' + M.base.id + '&target=' + M.target.id +
(f.ind ? '&indicator=' + encodeURIComponent(f.ind) : '') +
(f.from ? '&from=' + encodeURIComponent(f.from) + '&to=' + encodeURIComponent(f.to) : '');
}
function openList(f) {
var t = describe(f);
A.load(listURL(M.list_url, f), t.title).then(function (l) {
if (!l) return;
A.fill({
title: t.title, note: t.note, hints: t.hints, scope: 'Запуски', runLabel: 'A — ' + M.base.label + '; B — ' + M.target.label,
cols: l.columns, rows: l.rows, csvURL: listURL(M.csv_url, f)
});
});
}
/* ---- the page ---- */
function renderNote() {
function line(name, m, r, cancelled) {
return '<div>' + esc(name + ': запуск ' + m.id + ', ' + m.kind + ', начало ' + m.start + ', завершён ' + m.end + ', длительность ' + m.duration +
', ' + fmt(r.addresses) + ' адр.' + (r.rechecked ? ', перепроверено внутри запуска: ' + fmt(r.rechecked) : '') +
(cancelled ? ', отменено (не участвуют в сравнении): ' + fmt(cancelled) : '') + '.') + '</div>';
}
$('an-cmp-note').innerHTML = line('A', M.base, R.runs.base, R.cancelled.base) + line('B', M.target, R.runs.target, R.cancelled.target);
}
function renderKpis() {
var GO = ' <span class="an-go">список →</span>';
var tiles = [
['Новые', G.new, 'есть в B, не было в A', 'new'],
['Выбывшие', G.left, 'были в A, нет в B', 'left'],
['Общие', G.common, 'есть в обоих запусках', 'common'],
['Изменились', G.changed, pct(G.changed, G.common) + '% общих адресов', 'changed'],
['Без изменений', G.same, pct(G.same, G.common) + '% общих адресов', 'same']
];
$('an-cmp-kpis').innerHTML = tiles.map(function (t) {
var inner = '<div class="an-k">' + t[0] + '</div><div class="an-v">' + fmt(t[1]) + '</div><div class="an-d">' + t[2] + (t[1] ? GO : '') + '</div>';
return t[1]
? '<button type="button" class="an-kpi" data-group="' + t[3] + '" aria-haspopup="dialog">' + inner + '</button>'
: '<div class="an-kpi">' + inner + '</div>';
}).join('');
}
/* a number that opens a list; zero is plain text */
function link(n, attrs, tip) {
return n ? '<button type="button" class="an-lnk" ' + attrs + ' ' + tipAttr(tip) + '>' + fmt(n) + '</button>' : '<span class="an-zero">0</span>';
}
function renderIndicators() {
var COLS = [['new', 'Новые', 'новые адреса, входящие в индикатор'], ['left', 'Выбывшие', 'выбывшие адреса, входившие в индикатор'],
['entered', 'Вошли', 'общие адреса, вошедшие в индикатор'], ['exited', 'Вышли', 'общие адреса, вышедшие из индикатора']];
$('an-cmp-ind').innerHTML = '<thead><tr><th>Индикатор</th><th class="an-r">A</th><th class="an-r">B</th><th class="an-r">Δ</th>' +
COLS.map(function (c) { return '<th class="an-r">' + c[1] + '</th>'; }).join('') + '</tr></thead><tbody>' +
R.indicators.map(function (d) {
var good = d.key === 'verdict_pass' ? d.delta > 0 : d.delta < 0;
var delta = d.delta > 0 ? '+' + fmt(d.delta) : d.delta < 0 ? '−' + fmt(-d.delta) : '0';
return '<tr><td>' + esc(d.name) + '</td><td class="an-num an-r">' + fmt(d.base) + '</td><td class="an-num an-r">' + fmt(d.target) + '</td>' +
'<td class="an-num an-r' + (d.delta ? (good ? ' an-good' : ' an-bad') : '') + '">' + delta + '</td>' +
COLS.map(function (c) {
return '<td class="an-num an-r">' + link(d[c[0]], 'data-group="' + c[0] + '" data-ind="' + d.key + '"', d.name + ': ' + c[2]) + '</td>';
}).join('') + '</tr>';
}).join('') + '</tbody>';
}
function renderTransitions() {
var V = T.verdicts;
$('an-cmp-tr').innerHTML = '<thead><tr><th></th>' + V.map(function (v) { return '<th class="an-r">B: ' + esc(v) + '</th>'; }).join('') + '<th class="an-r">нет в B</th></tr></thead><tbody>' +
V.map(function (from, i) {
return '<tr><td>A: ' + esc(from) + '</td>' + V.map(function (to, j) {
return '<td class="an-num an-r">' + link(T.matrix[i][j], 'data-group="common" data-from="' + from + '" data-to="' + to + '"', 'вердикт ' + from + ' в A → ' + to + ' в B') + '</td>';
}).join('') + '<td class="an-num an-r">' + link(T.left[i], 'data-group="left" data-ind="verdict_' + from + '"', 'выбыли, вердикт в A: ' + from) + '</td></tr>';
}).join('') +
'<tr><td>нет в A</td>' + V.map(function (to, j) {
return '<td class="an-num an-r">' + link(T.new[j], 'data-group="new" data-ind="verdict_' + to + '"', 'новые, вердикт в B: ' + to) + '</td>';
}).join('') + '<td class="an-num an-r">—</td></tr></tbody>';
}
/* ---- wiring ---- */
function onOpen(e) {
var b = e.target.closest('[data-group]');
if (b) openList({ group: b.dataset.group, ind: b.dataset.ind || '', from: b.dataset.from || '', to: b.dataset.to || '' });
}
renderNote(); renderKpis(); renderIndicators(); renderTransitions();
['an-cmp-kpis', 'an-cmp-ind', 'an-cmp-tr'].forEach(function (id) { $(id).addEventListener('click', onOpen); });
})();