добавлена оптимизированная версия нового дизайна
This commit is contained in:
@@ -25,7 +25,6 @@ public static class BpmnBuilder
|
||||
{
|
||||
var visitor = new BpmnVisitor(diagram);
|
||||
fragment.Accept(visitor);
|
||||
visitor.Diagram.AddMissingProcessEdges();
|
||||
return visitor.Diagram;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ public class SqlDiagramProcessor : ISqlDiagramProcessor
|
||||
}
|
||||
|
||||
BpmnBuilder.Build(fragment, _bpmnDiagram);
|
||||
_bpmnDiagram.AddMissingProcessEdges();
|
||||
}
|
||||
|
||||
private Stream GetFileContents(string filePath)
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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, "&")
|
||||
.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();
|
||||
|
||||
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 });
|
||||
|
||||
|
||||
});
|
||||
@@ -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
1895
SQLLinter/Infrastructure/Reporters/Static/HtmlFormatterOld.js
Normal file
1895
SQLLinter/Infrastructure/Reporters/Static/HtmlFormatterOld.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user