добавлена оптимизированная версия нового дизайна

This commit is contained in:
FrigaT
2025-12-28 11:53:21 +03:00
parent f988d9af1e
commit e4acae11f0
15 changed files with 9263 additions and 3342 deletions

View File

@@ -25,7 +25,6 @@ public static class BpmnBuilder
{
var visitor = new BpmnVisitor(diagram);
fragment.Accept(visitor);
visitor.Diagram.AddMissingProcessEdges();
return visitor.Diagram;
}

View File

@@ -58,6 +58,7 @@ public class SqlDiagramProcessor : ISqlDiagramProcessor
}
BpmnBuilder.Build(fragment, _bpmnDiagram);
_bpmnDiagram.AddMissingProcessEdges();
}
private Stream GetFileContents(string filePath)

View File

@@ -2,9 +2,9 @@
using SQLLinter.Infrastructure.Diagram;
using System.Text;
namespace SQLLinter.Infrastructure.Reporters;
namespace SQLLinter.Infrastructure.Reporters.Formatters.Html.v1;
public class HtmlReportFormatter_v1 : IReportFormatter
public class HtmlReportFormatter : IReportFormatter
{
public string Format(List<IRuleViolation> violations)
=> Format(violations, null);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SQLLinter.Infrastructure.Reporters;
namespace SQLLinter.Infrastructure.Reporters.Formatters.Html.v2;
public class HtmlReportFormatter_v2 : IReportFormatter
public class HtmlReportFormatter : IReportFormatter
{
public string Format(List<IRuleViolation> violations)
=> Format(violations, null);
@@ -168,7 +168,7 @@ public class HtmlReportFormatter_v2 : IReportFormatter
<style>
""");
sb.AppendLine(LoadResource("HtmlFormatter.css"));
sb.AppendLine(LoadResource("HtmlFormatter_v2.css"));
sb.AppendLine("</style></head><body><main id=\"main-content\">");
}
@@ -186,7 +186,7 @@ public class HtmlReportFormatter_v2 : IReportFormatter
""");
// Загружаем основной JS
sb.AppendLine(LoadResource("HtmlFormatter.js"));
sb.AppendLine(LoadResource("HtmlFormatter_v2.js"));
sb.AppendLine("""
</script>

View File

@@ -0,0 +1,542 @@
/* ---------------------------------------------------------
FLUENT UI 2 — BASE VARIABLES
--------------------------------------------------------- */
:root {
--color-primary: #0f6cbd;
--color-primary-hover: #115ea3;
--color-primary-light: #d0e7ff;
--color-bg: #ffffff;
--color-bg-alt: #f5f5f5;
--color-bg-neutral: #f3f2f1;
--color-border: #d1d1d1;
--color-border-strong: #b5b5b5;
--color-text: #1a1a1a;
--color-text-secondary: #5e5e5e;
--color-text-tertiary: #8a8a8a;
--color-critical: #d13438;
--color-critical-bg: #f8d7da;
--color-warning: #ffaa44;
--color-warning-bg: #fff4ce;
--color-info: #0f6cbd;
--color-info-bg: #d0e7ff;
--radius-s: 4px;
--radius-m: 6px;
--radius-l: 8px;
--space-xs: 4px;
--space-s: 8px;
--space-m: 12px;
--space-l: 16px;
--space-xl: 20px;
--space-xxl: 28px;
--font-s: 12px;
--font-m: 14px;
--font-l: 16px;
--font-xl: 18px;
--z-tabs: 1000;
--z-header: 900;
}
/* ---------------------------------------------------------
RESET
--------------------------------------------------------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Segoe UI", sans-serif;
background: var(--color-bg-neutral);
color: var(--color-text);
line-height: 1.5;
}
/* ---------------------------------------------------------
FILE REPORT WRAPPER
--------------------------------------------------------- */
.file-report {
display: none;
padding-bottom: 120px;
}
.file-report.active {
display: block;
}
/* ---------------------------------------------------------
FILE TITLE (STICKY)
--------------------------------------------------------- */
.file-title-container {
position: sticky;
top: 0;
z-index: var(--z-header);
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
padding: var(--space-l) var(--space-xl);
}
.file-title {
font-size: var(--font-xl);
font-weight: 600;
color: var(--color-text);
}
/* ---------------------------------------------------------
SEVERITY SECTIONS (FLUENT UI 2 STYLE)
--------------------------------------------------------- */
.severity-section {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-l);
padding: var(--space-xl);
margin: var(--space-xl) var(--space-xl);
}
.severity-section.critical {
border-left: 4px solid var(--color-critical);
}
.severity-section.warning {
border-left: 4px solid var(--color-warning);
}
.severity-section.info {
border-left: 4px solid var(--color-info);
}
.severity-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-l);
padding-bottom: var(--space-s);
border-bottom: 1px solid var(--color-border);
}
.severity-title h2 {
font-size: var(--font-l);
font-weight: 600;
}
.severity-count {
background: var(--color-bg-alt);
padding: var(--space-xs) var(--space-s);
border-radius: var(--radius-s);
font-size: var(--font-s);
color: var(--color-text-secondary);
}
/* ---------------------------------------------------------
GRID TABLE (REPLACES <table>)
--------------------------------------------------------- */
.grid-table {
display: grid;
gap: var(--space-xs);
}
.grid-header,
.grid-row {
display: grid;
grid-template-columns: 40px 60px 60px 250px 1fr;
gap: var(--space-xs);
padding: var(--space-s);
border-radius: var(--radius-s);
}
.grid-header {
background: var(--color-bg-alt);
font-weight: 600;
color: var(--color-text-secondary);
font-size: var(--font-s);
}
.grid-row {
background: var(--color-bg);
border: 1px solid var(--color-border);
font-size: var(--font-m);
transition: background-color 0.15s ease;
}
.grid-row:hover {
background: var(--color-bg-alt);
}
/* ---------------------------------------------------------
SUMMARY SECTION
--------------------------------------------------------- */
.summary-section {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-l);
padding: var(--space-xl);
margin: var(--space-xl);
}
.summary-header h2 {
font-size: var(--font-l);
margin-bottom: var(--space-m);
}
.summary-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--space-m);
margin-bottom: var(--space-l);
}
.stat-card {
background: var(--color-bg-alt);
border-radius: var(--radius-m);
padding: var(--space-m);
text-align: center;
}
.stat-value {
font-size: var(--font-xl);
font-weight: 600;
}
.stat-label {
font-size: var(--font-s);
color: var(--color-text-secondary);
}
/* Progress bar */
.progress-bar {
display: flex;
height: 8px;
border-radius: var(--radius-s);
overflow: hidden;
background: var(--color-border);
}
.progress-fill.critical {
background: var(--color-critical);
}
.progress-fill.warning {
background: var(--color-warning);
}
.progress-fill.info {
background: var(--color-info);
}
/* ---------------------------------------------------------
FILE CARDS IN SUMMARY
--------------------------------------------------------- */
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-m);
}
.file-card {
background: var(--color-bg-alt);
border-radius: var(--radius-m);
padding: var(--space-m);
transition: background-color 0.2s ease, transform 0.2s ease;
}
.file-card:hover {
transform: translateY(-2px);
background: var(--color-bg-neutral);
}
.file-name-small {
font-weight: 600;
font-size: var(--font-m);
}
.file-card-bottom {
display: flex;
justify-content: space-between;
margin-top: var(--space-s);
}
.file-violations {
display: flex;
gap: var(--space-xs);
}
.violation-badge {
padding: var(--space-xs) var(--space-s);
border-radius: var(--radius-s);
font-size: var(--font-s);
color: var(--color-text);
}
.violation-badge.critical {
background: var(--color-critical-bg);
color: var(--color-critical);
}
.violation-badge.warning {
background: var(--color-warning-bg);
color: var(--color-warning);
}
.violation-badge.info {
background: var(--color-info-bg);
color: var(--color-info);
}
/* ---------------------------------------------------------
TABS CONTAINER (STICKY BOTTOM)
--------------------------------------------------------- */
.tabs-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-bg);
border-top: 1px solid var(--color-border);
padding: var(--space-s) var(--space-l);
z-index: var(--z-tabs);
}
/* ---------------------------------------------------------
TABS — HORIZONTAL SCROLL + VISIBLE SCROLLBAR
--------------------------------------------------------- */
.tabs {
display: flex;
flex-wrap: nowrap;
gap: var(--space-s);
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
scroll-behavior: smooth;
padding-bottom: 4px;
/* Firefox scrollbar */
scrollbar-width: thin;
scrollbar-color: var(--color-border-strong) var(--color-bg-alt);
}
/* Chrome / Edge / Safari scrollbar */
.tabs::-webkit-scrollbar {
height: 8px;
}
.tabs::-webkit-scrollbar-track {
background: var(--color-bg-alt);
border-radius: 4px;
}
.tabs::-webkit-scrollbar-thumb {
background: var(--color-border-strong);
border-radius: 4px;
}
.tabs::-webkit-scrollbar-thumb:hover {
background: var(--color-text-tertiary);
}
/* ---------------------------------------------------------
TAB BASE STYLES (COMPACT, AUTO WIDTH, MAX 2 LINES)
--------------------------------------------------------- */
.tab {
flex: 0 0 auto; /* растягивается под контент, не ломая одну линию */
min-width: 0;
padding: 6px 12px;
line-height: 1.25;
display: flex;
align-items: flex-start;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: var(--radius-m);
cursor: pointer;
font-size: var(--font-m);
font-weight: 500;
color: var(--color-text-secondary);
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.tab:hover {
background: var(--color-bg-neutral);
transform: translateY(-1px);
}
.tab.active {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
transform: translateY(-2px);
}
/* Внутренний layout таба */
.tab-inner {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
width: 100%;
}
/* Текст таба — максимум 2 строки, с переносами */
.tab-text {
font-size: 13px;
line-height: 1.25;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
max-height: calc(1.25em * 2);
overflow: hidden;
}
/* Счётчики — компактные */
.tab-counters {
display: flex;
flex-direction: column;
gap: 2px;
flex-shrink: 0;
}
.tab-counter {
font-size: 10px;
min-width: 14px;
height: 14px;
padding: 0 3px;
line-height: 14px;
border-radius: var(--radius-s);
text-align: center;
}
.tab-counter.critical {
background: var(--color-critical-bg);
color: var(--color-critical);
}
.tab-counter.warning {
background: var(--color-warning-bg);
color: var(--color-warning);
}
.tab-counter.info {
background: var(--color-info-bg);
color: var(--color-info);
}
.tab-counter.empty {
opacity: 0.3;
}
/* Мобильная компактность */
@media (max-width: 480px) {
.tab {
padding: 4px 8px;
}
.tab-text {
font-size: 12px;
max-height: calc(1.2em * 2);
-webkit-line-clamp: 2;
}
.tab-counter {
min-width: 12px;
height: 12px;
font-size: 9px;
padding: 0 2px;
}
}
/* ---------------------------------------------------------
FLUENT UI 2 — DARK THEME (PREFERS-COLOR-SCHEME)
--------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1f1f1f;
--color-bg-alt: #2a2a2a;
--color-bg-neutral: #2d2d2d;
--color-text: #f3f3f3;
--color-text-secondary: #c8c8c8;
--color-text-tertiary: #9a9a9a;
--color-border: #3a3a3a;
--color-border-strong: #4a4a4a;
--color-primary: #3aa0f3;
--color-primary-hover: #2899f5;
--color-primary-light: #0f2b4d;
--color-critical-bg: #4c191b;
--color-warning-bg: #4c3b1a;
--color-info-bg: #0f2b4d;
}
.file-title-container {
background: var(--color-bg);
}
.severity-section {
background: var(--color-bg);
border-color: var(--color-border);
}
.grid-row {
background: var(--color-bg-alt);
border-color: var(--color-border);
}
.grid-row:hover {
background: var(--color-bg-neutral);
}
.summary-section {
background: var(--color-bg);
border-color: var(--color-border);
}
.file-card {
background: var(--color-bg-alt);
}
.tabs-container {
background: var(--color-bg);
border-color: var(--color-border);
}
.tab {
background: var(--color-bg-alt);
border-color: var(--color-border);
color: var(--color-text-secondary);
}
.tab.active {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
}
/* Dark scrollbar colors */
.tabs {
scrollbar-color: var(--color-border-strong) var(--color-bg);
}
.tabs::-webkit-scrollbar-track {
background: var(--color-bg);
}
.tabs::-webkit-scrollbar-thumb {
background: var(--color-border-strong);
}
.tabs::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
}
/* ---------------------------------------------------------
TAB TRANSITIONS & FILE REPORT ANIMATION
--------------------------------------------------------- */
.file-report {
opacity: 0;
transform: translateY(6px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.file-report.active {
opacity: 1;
transform: translateY(0);
}

View File

@@ -0,0 +1,486 @@
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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();
const tabs = document.querySelector('.tabs');
tabs.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
tabs.scrollBy({
left: e.deltaY * 4,
behavior: "smooth"
});
}
}, { passive: false });
});

View File

@@ -0,0 +1,291 @@
using SQLLinter.Common;
using SQLLinter.Infrastructure.Diagram;
using SQLLinter.Infrastructure.Rules.RuleViolations;
using System.Data;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SQLLinter.Infrastructure.Reporters.Formatters.Html.v3;
public class HtmlReportFormatter : IReportFormatter
{
public string Format(List<IRuleViolation> violations)
=> Format(violations, null);
public string Format(List<IRuleViolation> violations, BpmnDiagram? diagram)
{
var sb = new StringBuilder();
GenerateBeginningHtml(sb);
// Подготовка данных для передачи в JS
var reportData = PrepareReportData(violations, diagram);
var jsonData = JsonSerializer.Serialize(reportData, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
if (violations.Count == 0 && diagram == null)
{
// Случай без нарушений
sb.AppendLine("<div class=\"no-violations\">");
sb.AppendLine("<div class=\"no-violations-content\">");
sb.AppendLine("<div class=\"no-violations-icon\">✅</div>");
sb.AppendLine("<h3 class=\"no-violations-title\">Проверка завершена</h3>");
sb.AppendLine("<p class=\"no-violations-description\">Нарушений правил SQL не обнаружено.</p>");
sb.AppendLine("</div>");
sb.AppendLine("</div>");
GenerateEndingHtml(sb, false, HtmlMinifier.CompressJson(jsonData));
return sb.ToString();
}
// Основной контейнер для отчета
sb.AppendLine("""
<!-- Основной контейнер отчёта -->
<main id="main-content">
<!-- Контейнер для всех отчётов -->
<div id="reports-container"></div>
</main>
<!-- Sticky Tabs -->
<div class="tabs-container">
<div class="tabs" id="tabs-list"></div>
</div>
""");
GenerateEndingHtml(sb, diagram != null, jsonData);
var html = HtmlMinifier.MinifyHtml(sb.ToString());
return html;
}
private ReportData PrepareReportData(List<IRuleViolation> violations, BpmnDiagram? diagram)
{
var reportData = new ReportData();
// Группировка по файлам
var groupedByFile = violations
.GroupBy(v => v.FileName)
.OrderBy(g => g.Key)
.ToList();
foreach (var fileGroup in groupedByFile)
{
var fileData = new FileReport
{
Name = fileGroup.Key,
};
// Группировка по severity
var severityGroups = fileGroup
.GroupBy(v => v.Severity)
.OrderByDescending(g => g.Key)
.ToList();
foreach (var violation in fileGroup.Select(t => t).OrderBy(v => v.Line).ThenBy(v => v.Column))
{
int ruleId;
List<string> args = new();
if (violation is RuleTemplateViolation templateRule)
{
args = templateRule.Params.Select(p => EscapeHtml(p)).ToList();
if (reportData.Rules.Any(t => t.Value.Name == templateRule.RuleName))
{
ruleId = reportData.Rules.First(t => t.Value.Name == templateRule.RuleName).Key;
}
else
{
ruleId = reportData.Rules.Count + 1;
reportData.Rules.Add(ruleId, new Rule
{
Name = templateRule.RuleName,
Template = templateRule.RuleTemplate
});
}
}
else
{
ruleId = reportData.Rules.Count + 1;
reportData.Rules.Add(ruleId, new Rule
{
Name = violation.RuleName,
Template = violation.Text,
});
}
var v = new Violation()
{
RuleId = ruleId,
Args = args,
Column = violation.Column,
Line = violation.Line,
};
if (violation.Severity == RuleViolationSeverity.Critical)
{
v.Index = fileData.Violations.Critical.Count + 1;
fileData.Violations.Critical.Add(v);
}
else if (violation.Severity == RuleViolationSeverity.Warning)
{
v.Index = fileData.Violations.Warning.Count + 1;
fileData.Violations.Warning.Add(v);
}
else if (violation.Severity == RuleViolationSeverity.Info)
{
v.Index = fileData.Violations.Info.Count + 1;
fileData.Violations.Info.Add(v);
}
}
reportData.Files.Add(fileData);
}
// Добавление диаграммы, если есть
if (diagram != null)
{
reportData.Diagram = new Diagram
{
Content = MermaidRenderer.ToMermaidContent(diagram),
HasDiagram = true
};
}
return reportData;
}
private void GenerateBeginningHtml(StringBuilder sb)
{
sb.AppendLine("""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Отчёт по SQLпроверкам</title>
<style>
""");
sb.AppendLine(LoadResource("HtmlFormatter_v3.css"));
sb.AppendLine("</style></head><body><main id=\"main-content\">");
}
private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram, string jsonData)
{
// Вставка JSON данных
sb.AppendLine($"""
<script id="report-data" type="application/json">
{jsonData}
</script>
""");
sb.AppendLine("""
<script type="module">
""");
// Загружаем основной JS
sb.AppendLine(LoadResource("HtmlFormatter_v3.js"));
sb.AppendLine("""
</script>
</body>
</html>
""");
}
private static string LoadResource(string endsWith)
{
var assembly = Assembly.GetExecutingAssembly();
var name = assembly.GetManifestResourceNames()
.First(n => n.EndsWith(endsWith, StringComparison.OrdinalIgnoreCase));
using var stream = assembly.GetManifestResourceStream(name);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
private static string EscapeHtml(string text)
{
return System.Net.WebUtility.HtmlEncode(text);
}
// Классы для сериализации
private class ReportData
{
[JsonPropertyName("f")] // files
public List<FileReport> Files { get; set; } = new();
[JsonPropertyName("r")] // rules
public Dictionary<int, Rule> Rules { get; set; } = new();
[JsonPropertyName("d")] // diagram
public Diagram Diagram { get; set; } = new();
}
private class FileReport
{
[JsonPropertyName("n")] // name
public string Name { get; set; } = string.Empty;
[JsonPropertyName("v")] // violations
public Violations Violations { get; set; } = new();
}
private class Violations
{
[JsonPropertyName("c")] // critical
public List<Violation> Critical { get; set; } = new();
[JsonPropertyName("w")] // warning
public List<Violation> Warning { get; set; } = new();
[JsonPropertyName("i")] // info
public List<Violation> Info { get; set; } = new();
}
private class Violation
{
[JsonPropertyName("i")] // index
public int Index { get; set; }
[JsonPropertyName("l")] // line
public int Line { get; set; }
[JsonPropertyName("c")] // column
public int Column { get; set; }
[JsonPropertyName("r")] // ruleId
public int RuleId { get; set; }
[JsonPropertyName("a")] // args (optional)
public List<string>? Args { get; set; }
}
private class Rule
{
[JsonPropertyName("n")] // name
public string Name { get; set; } = string.Empty;
[JsonPropertyName("t")] // template
public string Template { get; set; } = string.Empty;
}
private class Diagram
{
[JsonPropertyName("c")] // content
public string Content { get; set; } = string.Empty;
[JsonPropertyName("h")] // hasDiagram
public bool HasDiagram { get; set; }
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,12 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Infrastructure\Reporters\Formatters\Html\v2\HtmlFormatter_v2.js" />
<EmbeddedResource Include="Infrastructure\Reporters\Formatters\Html\v2\HtmlFormatter_v2.css" />
<EmbeddedResource Include="Infrastructure\Reporters\Formatters\Html\v3\HtmlFormatter_v3.css" />
<EmbeddedResource Include="Infrastructure\Reporters\Formatters\Html\v3\HtmlFormatter_v3.js" />
<EmbeddedResource Include="Infrastructure\Reporters\Static\HtmlFormatter - Копировать.css" />
<EmbeddedResource Include="Infrastructure\Reporters\Static\HtmlFormatterOld.js" />
<EmbeddedResource Include="Infrastructure\Reporters\Static\HtmlFormatter.css" />
<EmbeddedResource Include="Infrastructure\Reporters\Static\HtmlFormatter.js" />
</ItemGroup>