472 lines
15 KiB
JavaScript
472 lines
15 KiB
JavaScript
document.addEventListener("DOMContentLoaded", function () {
|
||
const dataEl = document.getElementById("report-data");
|
||
if (!dataEl) return console.error("report-data not found");
|
||
|
||
const data = JSON.parse(dataEl.textContent || "{}");
|
||
const files = data.files || data.f || [];
|
||
const rules = data.rules || data.r || {};
|
||
|
||
const tabsList = document.getElementById("tabs-list");
|
||
const reportsContainer = document.getElementById("reports-container");
|
||
|
||
if (!tabsList || !reportsContainer) {
|
||
console.error("Missing tabs-list or reports-container");
|
||
return;
|
||
}
|
||
|
||
/* ---------------------------------------------------------
|
||
UTILS
|
||
--------------------------------------------------------- */
|
||
|
||
function escapeHtml(str) {
|
||
if (str == null) return "";
|
||
return String(str)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function countViolations(v) {
|
||
return {
|
||
c: v?.c?.length || 0,
|
||
w: v?.w?.length || 0,
|
||
i: v?.i?.length || 0
|
||
};
|
||
}
|
||
|
||
const rendered = new Set(); // ленивый рендер
|
||
|
||
/* ---------------------------------------------------------
|
||
RENDER TABS
|
||
--------------------------------------------------------- */
|
||
|
||
function renderTabs() {
|
||
const frag = document.createDocumentFragment();
|
||
|
||
files.forEach((file, index) => {
|
||
const v = countViolations(file.v);
|
||
|
||
const btn = document.createElement("button");
|
||
btn.className = "tab";
|
||
btn.dataset.target = `file_${index}`;
|
||
btn.dataset.index = index;
|
||
|
||
const inner = document.createElement("div");
|
||
inner.className = "tab-inner";
|
||
|
||
const text = document.createElement("span");
|
||
text.className = "tab-text";
|
||
text.title = file.n;
|
||
text.textContent = file.n;
|
||
|
||
const counters = document.createElement("div");
|
||
counters.className = "tab-counters";
|
||
|
||
const c1 = document.createElement("span");
|
||
c1.className = "tab-counter critical" + (v.c ? "" : " empty");
|
||
c1.textContent = v.c || "";
|
||
|
||
const c2 = document.createElement("span");
|
||
c2.className = "tab-counter warning" + (v.w ? "" : " empty");
|
||
c2.textContent = v.w || "";
|
||
|
||
const c3 = document.createElement("span");
|
||
c3.className = "tab-counter info" + (v.i ? "" : " empty");
|
||
c3.textContent = v.i || "";
|
||
|
||
counters.append(c1, c2, c3);
|
||
inner.append(text, counters);
|
||
btn.append(inner);
|
||
frag.append(btn);
|
||
});
|
||
|
||
// Summary tab
|
||
const summaryBtn = document.createElement("button");
|
||
summaryBtn.className = "tab";
|
||
summaryBtn.dataset.target = "summary_report";
|
||
|
||
const inner = document.createElement("div");
|
||
inner.className = "tab-inner";
|
||
|
||
const text = document.createElement("span");
|
||
text.className = "tab-text";
|
||
text.textContent = "Summary";
|
||
|
||
inner.append(text);
|
||
summaryBtn.append(inner);
|
||
frag.append(summaryBtn);
|
||
|
||
tabsList.innerHTML = "";
|
||
tabsList.append(frag);
|
||
}
|
||
|
||
/* ---------------------------------------------------------
|
||
GRID ROW CREATOR (VIOLATIONS)
|
||
--------------------------------------------------------- */
|
||
|
||
function createGridRow(v, rule) {
|
||
const row = document.createElement("div");
|
||
row.className = "grid-row";
|
||
|
||
const cIndex = document.createElement("div");
|
||
cIndex.textContent = v.i;
|
||
|
||
const cLine = document.createElement("div");
|
||
cLine.textContent = v.l;
|
||
|
||
const cCol = document.createElement("div");
|
||
cCol.textContent = v.c;
|
||
|
||
const cRule = document.createElement("div");
|
||
cRule.textContent = rule.n;
|
||
|
||
let text = rule.t || "";
|
||
const args = v.a || v.args;
|
||
if (Array.isArray(args)) {
|
||
for (let i = 0; i < args.length; i++) {
|
||
text = text.replace(`{${i}}`, args[i]);
|
||
}
|
||
}
|
||
|
||
const cDesc = document.createElement("div");
|
||
cDesc.textContent = text;
|
||
|
||
row.append(cIndex, cLine, cCol, cRule, cDesc);
|
||
return row;
|
||
}
|
||
|
||
/* ---------------------------------------------------------
|
||
FILE REPORT
|
||
--------------------------------------------------------- */
|
||
|
||
function renderFileReport(index) {
|
||
const file = files[index];
|
||
const v = file.v || {};
|
||
|
||
const root = document.createElement("div");
|
||
|
||
// Title
|
||
const titleWrap = document.createElement("div");
|
||
titleWrap.className = "file-title-container";
|
||
|
||
const title = document.createElement("div");
|
||
title.className = "file-title";
|
||
|
||
const name = document.createElement("span");
|
||
name.className = "file-name";
|
||
name.textContent = file.n;
|
||
|
||
title.append(name);
|
||
titleWrap.append(title);
|
||
root.append(titleWrap);
|
||
|
||
// Sections
|
||
function addSection(label, list, cls) {
|
||
if (!Array.isArray(list) || list.length === 0) return;
|
||
|
||
const section = document.createElement("div");
|
||
section.className = `severity-section ${cls}`;
|
||
|
||
const header = document.createElement("div");
|
||
header.className = "severity-header";
|
||
|
||
const hTitle = document.createElement("div");
|
||
hTitle.className = "severity-title";
|
||
|
||
const h2 = document.createElement("h2");
|
||
h2.textContent = label;
|
||
|
||
const count = document.createElement("span");
|
||
count.className = "severity-count";
|
||
count.textContent = list.length;
|
||
|
||
hTitle.append(h2, count);
|
||
header.append(hTitle);
|
||
section.append(header);
|
||
|
||
const grid = document.createElement("div");
|
||
grid.className = "grid-table";
|
||
|
||
// header row
|
||
const headerRow = document.createElement("div");
|
||
headerRow.className = "grid-header";
|
||
|
||
["#", "Строка", "Колонка", "Правило", "Описание"].forEach(t => {
|
||
const cell = document.createElement("div");
|
||
cell.textContent = t;
|
||
headerRow.append(cell);
|
||
});
|
||
|
||
grid.append(headerRow);
|
||
|
||
// rows
|
||
for (let i = 0; i < list.length; i++) {
|
||
const vItem = list[i];
|
||
const rule = rules[vItem.r] || { n: "Unknown", t: "Описание отсутствует" };
|
||
grid.append(createGridRow(vItem, rule));
|
||
}
|
||
|
||
section.append(grid);
|
||
root.append(section);
|
||
}
|
||
|
||
addSection("Critical", v.c, "critical");
|
||
addSection("Warning", v.w, "warning");
|
||
addSection("Info", v.i, "info");
|
||
|
||
return root;
|
||
}
|
||
|
||
/* ---------------------------------------------------------
|
||
SUMMARY
|
||
--------------------------------------------------------- */
|
||
|
||
function renderSummary() {
|
||
let totalC = 0, totalW = 0, totalI = 0;
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const v = countViolations(files[i].v);
|
||
totalC += v.c;
|
||
totalW += v.w;
|
||
totalI += v.i;
|
||
}
|
||
|
||
const total = totalC + totalW + totalI || 1;
|
||
|
||
const root = document.createElement("div");
|
||
|
||
// Title
|
||
const titleWrap = document.createElement("div");
|
||
titleWrap.className = "file-title-container";
|
||
|
||
const title = document.createElement("div");
|
||
title.className = "file-title";
|
||
|
||
const name = document.createElement("span");
|
||
name.className = "file-name";
|
||
name.textContent = "Сводный отчет";
|
||
|
||
title.append(name);
|
||
titleWrap.append(title);
|
||
root.append(titleWrap);
|
||
|
||
// Stats
|
||
const section = document.createElement("div");
|
||
section.className = "summary-section";
|
||
|
||
const header = document.createElement("div");
|
||
header.className = "summary-header";
|
||
|
||
const hTitle = document.createElement("div");
|
||
hTitle.className = "summary-title";
|
||
|
||
const h2 = document.createElement("h2");
|
||
h2.textContent = "Общая статистика";
|
||
|
||
hTitle.append(h2);
|
||
header.append(hTitle);
|
||
section.append(header);
|
||
|
||
const grid = document.createElement("div");
|
||
grid.className = "summary-stats-grid";
|
||
|
||
function statCard(cls, value, label) {
|
||
const card = document.createElement("div");
|
||
card.className = `stat-card ${cls}`;
|
||
|
||
const v = document.createElement("div");
|
||
v.className = "stat-value";
|
||
v.textContent = value;
|
||
|
||
const l = document.createElement("div");
|
||
l.className = "stat-label";
|
||
l.textContent = label;
|
||
|
||
card.append(v, l);
|
||
return card;
|
||
}
|
||
|
||
grid.append(
|
||
statCard("success", files.length, "Всего файлов"),
|
||
statCard("critical", totalC, "Critical нарушений"),
|
||
statCard("warning", totalW, "Warning нарушений"),
|
||
statCard("info", totalI, "Info нарушений")
|
||
);
|
||
|
||
section.append(grid);
|
||
|
||
// Progress
|
||
const dist = document.createElement("div");
|
||
dist.className = "violation-distribution";
|
||
|
||
const bar = document.createElement("div");
|
||
bar.className = "progress-bar";
|
||
|
||
const pc = (totalC / total) * 100;
|
||
const pw = (totalW / total) * 100;
|
||
const pi = (totalI / total) * 100;
|
||
|
||
const f1 = document.createElement("div");
|
||
f1.className = "progress-fill critical";
|
||
f1.style.width = pc + "%";
|
||
|
||
const f2 = document.createElement("div");
|
||
f2.className = "progress-fill warning";
|
||
f2.style.width = pw + "%";
|
||
|
||
const f3 = document.createElement("div");
|
||
f3.className = "progress-fill info";
|
||
f3.style.width = pi + "%";
|
||
|
||
bar.append(f1, f2, f3);
|
||
dist.append(bar);
|
||
|
||
section.append(dist);
|
||
root.append(section);
|
||
|
||
// Files overview
|
||
const section2 = document.createElement("div");
|
||
section2.className = "summary-section files-overview";
|
||
|
||
const header2 = document.createElement("div");
|
||
header2.className = "summary-header";
|
||
|
||
const hTitle2 = document.createElement("div");
|
||
hTitle2.className = "summary-title";
|
||
|
||
const h22 = document.createElement("h2");
|
||
h22.textContent = "Статистика файлов";
|
||
|
||
const count = document.createElement("span");
|
||
count.className = "severity-count";
|
||
count.textContent = files.length;
|
||
|
||
hTitle2.append(h22, count);
|
||
header2.append(hTitle2);
|
||
section2.append(header2);
|
||
|
||
const filesGrid = document.createElement("div");
|
||
filesGrid.className = "files-grid";
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const f = files[i];
|
||
const v = countViolations(f.v);
|
||
const t = v.c + v.w + v.i || 1;
|
||
|
||
const card = document.createElement("div");
|
||
card.className = "file-card";
|
||
|
||
const header = document.createElement("div");
|
||
header.className = "file-card-header";
|
||
|
||
const name = document.createElement("span");
|
||
name.className = "file-name-small";
|
||
name.textContent = f.n;
|
||
|
||
header.append(name);
|
||
card.append(header);
|
||
|
||
const bar = document.createElement("div");
|
||
bar.className = "progress-bar small";
|
||
|
||
const fc = document.createElement("div");
|
||
fc.className = "progress-fill critical";
|
||
fc.style.width = (v.c / t * 100) + "%";
|
||
|
||
const fw = document.createElement("div");
|
||
fw.className = "progress-fill warning";
|
||
fw.style.width = (v.w / t * 100) + "%";
|
||
|
||
const fi = document.createElement("div");
|
||
fi.className = "progress-fill info";
|
||
fi.style.width = (v.i / t * 100) + "%";
|
||
|
||
bar.append(fc, fw, fi);
|
||
card.append(bar);
|
||
|
||
const bottom = document.createElement("div");
|
||
bottom.className = "file-card-bottom";
|
||
|
||
const total = document.createElement("span");
|
||
total.className = "file-total";
|
||
total.textContent = "Total: " + (v.c + v.w + v.i);
|
||
|
||
const badges = document.createElement("div");
|
||
badges.className = "file-violations";
|
||
|
||
function badge(cls, val) {
|
||
const b = document.createElement("span");
|
||
b.className = `violation-badge ${cls}`;
|
||
b.textContent = val;
|
||
return b;
|
||
}
|
||
|
||
badges.append(
|
||
badge("critical", v.c),
|
||
badge("warning", v.w),
|
||
badge("info", v.i)
|
||
);
|
||
|
||
bottom.append(total, badges);
|
||
card.append(bottom);
|
||
|
||
filesGrid.append(card);
|
||
}
|
||
|
||
section2.append(filesGrid);
|
||
root.append(section2);
|
||
|
||
return root;
|
||
}
|
||
|
||
/* ---------------------------------------------------------
|
||
TAB HANDLER
|
||
--------------------------------------------------------- */
|
||
|
||
tabsList.addEventListener("click", function (e) {
|
||
const tab = e.target.closest(".tab");
|
||
if (!tab) return;
|
||
|
||
const targetId = tab.dataset.target;
|
||
|
||
// activate tab
|
||
tabsList.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
|
||
tab.classList.add("active");
|
||
|
||
// hide all reports
|
||
reportsContainer.innerHTML = "";
|
||
|
||
// lazy render
|
||
let content;
|
||
if (!rendered.has(targetId)) {
|
||
if (targetId === "summary_report") {
|
||
content = renderSummary();
|
||
} else {
|
||
const index = Number(tab.dataset.index);
|
||
content = renderFileReport(index);
|
||
}
|
||
rendered.add(targetId);
|
||
} else {
|
||
// already rendered → but we removed DOM → re-render
|
||
if (targetId === "summary_report") {
|
||
content = renderSummary();
|
||
} else {
|
||
const index = Number(tab.dataset.index);
|
||
content = renderFileReport(index);
|
||
}
|
||
}
|
||
|
||
reportsContainer.append(content);
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
});
|
||
|
||
/* ---------------------------------------------------------
|
||
INIT
|
||
--------------------------------------------------------- */
|
||
|
||
renderTabs();
|
||
|
||
const first = tabsList.querySelector(".tab");
|
||
if (first) first.click();
|
||
}); |