@sfdxy/mule-lint 1.5.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,12 +49,22 @@ flowchart TB
49
49
  I[Table]
50
50
  J[JSON]
51
51
  K[SARIF]
52
+ L[HTML Dashboard]
53
+ M[CSV]
54
+ end
55
+
56
+ subgraph HTMLStack["HTML Report Stack"]
57
+ L --> N[Tailwind CSS]
58
+ L --> O[Chart.js]
59
+ L --> P[Tabulator]
52
60
  end
53
61
 
54
62
  A --> B
55
63
  D --> I
56
64
  D --> J
57
65
  D --> K
66
+ D --> L
67
+ D --> M
58
68
  ```
59
69
 
60
70
  ### Data Flow
@@ -259,9 +269,19 @@ Machine-readable for scripting:
259
269
  }
260
270
  ```
261
271
 
262
- ### HTML (Human Readable)
272
+ ### HTML (Interactive Dashboard)
273
+
274
+ Generates a modern, interactive single-page report with:
275
+
276
+ - **Dashboard View**: Summary cards, severity donut chart, top violated rules bar chart, issues by category
277
+ - **Issues Browser**: Full-width searchable table with multiselect filters
278
+ - **Frozen Headers**: Table header stays visible when scrolling
279
+ - **Export**: Download filtered results as CSV
280
+ - **Responsive**: Works on desktop and mobile
281
+
282
+ Built with **Tailwind CSS**, **Chart.js**, and **Tabulator**.
263
283
 
264
- Generates a visual report with summary cards and correct issue highlighting:
284
+ ![HTML Report Dashboard](docs/images/html-report-dashboard.png)
265
285
 
266
286
  ```bash
267
287
  mule-lint src/main/mule -f html -o report.html
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@sfdxy/mule-lint",
3
+ "version": "1.7.0",
4
+ "description": "Static analysis tool for MuleSoft applications - supports humans, AI agents, and CI/CD pipelines",
5
+ "author": "Avinava",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "mulesoft",
9
+ "mule",
10
+ "lint",
11
+ "static-analysis",
12
+ "xml",
13
+ "sarif",
14
+ "cli"
15
+ ],
16
+ "bin": {
17
+ "mule-lint": "./dist/bin/mule-lint.js"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "build:watch": "tsc --watch",
29
+ "test": "jest",
30
+ "test:watch": "jest --watch",
31
+ "test:coverage": "jest --coverage",
32
+ "lint": "eslint src/ --ext .ts",
33
+ "lint:fix": "eslint src/ --ext .ts --fix",
34
+ "format": "prettier --write \"src/**/*.{ts,js,json,md}\"",
35
+ "clean": "rm -rf dist",
36
+ "prepublishOnly": "npm run clean && npm run build && npm test",
37
+ "release": "git tag v$(node -p 'require(\"./package.json\").version') && git push origin v$(node -p 'require(\"./package.json\").version')"
38
+ },
39
+ "dependencies": {
40
+ "@xmldom/xmldom": "^0.8.10",
41
+ "chalk": "^4.1.2",
42
+ "commander": "^11.1.0",
43
+ "fast-glob": "^3.3.2",
44
+ "js-yaml": "^4.1.1",
45
+ "xpath": "^0.0.34"
46
+ },
47
+ "devDependencies": {
48
+ "@types/jest": "^29.5.11",
49
+ "@types/js-yaml": "^4.0.9",
50
+ "@types/node": "^20.19.27",
51
+ "@typescript-eslint/eslint-plugin": "^6.15.0",
52
+ "@typescript-eslint/parser": "^6.15.0",
53
+ "eslint": "^8.56.0",
54
+ "jest": "^29.7.0",
55
+ "prettier": "^3.7.4",
56
+ "ts-jest": "^29.1.1",
57
+ "typescript": "^5.3.3"
58
+ },
59
+ "engines": {
60
+ "node": ">=18.0.0"
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/Avinava/mule-lint.git"
65
+ },
66
+ "bugs": {
67
+ "url": "https://github.com/Avinava/mule-lint/issues"
68
+ },
69
+ "homepage": "https://github.com/Avinava/mule-lint#readme"
70
+ }
@@ -24,6 +24,7 @@ export declare class YamlParser {
24
24
  static isEncryptedValue(value: unknown): boolean;
25
25
  /**
26
26
  * Check if a key name suggests it contains sensitive data
27
+ * Uses word-boundary matching to avoid false positives
27
28
  */
28
29
  static isSensitiveKey(key: string): boolean;
29
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"YamlParser.d.ts","sourceRoot":"","sources":["../../../src/core/YamlParser.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,qBAAa,UAAU;IACnB;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IASlE;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQ7D;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAK5C;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE;IAiBtE;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAMhD;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAe9C"}
1
+ {"version":3,"file":"YamlParser.d.ts","sourceRoot":"","sources":["../../../src/core/YamlParser.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,qBAAa,UAAU;IACnB;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IASlE;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQ7D;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAK5C;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE;IAiBtE;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAMhD;;;OAGG;IACH,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAgD9C"}
@@ -99,21 +99,52 @@ class YamlParser {
99
99
  }
100
100
  /**
101
101
  * Check if a key name suggests it contains sensitive data
102
+ * Uses word-boundary matching to avoid false positives
102
103
  */
103
104
  static isSensitiveKey(key) {
104
- const sensitivePatterns = [
105
+ const lowerKey = key.toLowerCase();
106
+ // Patterns that indicate a secret - must be at word boundary (end of key or before .)
107
+ const secretKeyEndings = [
105
108
  'password',
109
+ 'passwd',
106
110
  'secret',
107
111
  'apikey',
108
112
  'api-key',
109
113
  'api_key',
110
- 'token',
111
- 'credential',
112
- 'private',
113
- 'auth',
114
+ 'accesstoken',
115
+ 'access-token',
116
+ 'access_token',
117
+ 'refreshtoken',
118
+ 'refresh-token',
119
+ 'refresh_token',
120
+ 'clientsecret',
121
+ 'client-secret',
122
+ 'client_secret',
123
+ 'privatekey',
124
+ 'private-key',
125
+ 'private_key',
126
+ 'credentials',
127
+ 'authtoken',
128
+ 'auth-token',
129
+ 'auth_token',
130
+ 'bearertoken',
131
+ 'bearer-token',
132
+ 'bearer_token',
133
+ 'consumerkey',
134
+ 'consumer-key',
135
+ 'consumer_key',
136
+ 'consumersecret',
137
+ 'consumer-secret',
138
+ 'consumer_secret',
139
+ 'tokensecret',
140
+ 'token-secret',
141
+ 'token_secret',
114
142
  ];
115
- const lowerKey = key.toLowerCase();
116
- return sensitivePatterns.some(pattern => lowerKey.includes(pattern));
143
+ // Get the last segment of the key (after the last .)
144
+ const segments = lowerKey.split('.');
145
+ const lastSegment = segments[segments.length - 1];
146
+ // Check if the last segment matches any secret pattern
147
+ return secretKeyEndings.some(pattern => lastSegment === pattern || lastSegment.endsWith(pattern));
117
148
  }
118
149
  }
119
150
  exports.YamlParser = YamlParser;
@@ -1 +1 @@
1
- {"version":3,"file":"YamlParser.js","sourceRoot":"","sources":["../../../src/core/YamlParser.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8CAAgC;AAChC,uCAAyB;AACzB,2CAA6B;AAE7B;;GAEG;AACH,MAAa,UAAU;IACnB;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,QAAgB;QAC7B,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAClD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA4B,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,OAAe;QACxB,IAAI,CAAC;YACD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA4B,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,QAAgB;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QACjD,OAAO,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,CAAC;IAC7C,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,GAA4B,EAAE,MAAM,GAAG,EAAE;QACvD,MAAM,IAAI,GAAa,EAAE,CAAC;QAE1B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;YAClD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YAEvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,KAAgC,EAAE,OAAO,CAAC,CAAC,CAAC;YAC7E,CAAC;iBAAM,CAAC;gBACJ,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,KAAc;QAClC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5C,8CAA8C;QAC9C,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,GAAW;QAC7B,MAAM,iBAAiB,GAAG;YACtB,UAAU;YACV,QAAQ;YACR,QAAQ;YACR,SAAS;YACT,SAAS;YACT,OAAO;YACP,YAAY;YACZ,SAAS;YACT,MAAM;SACT,CAAC;QACF,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QACnC,OAAO,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACzE,CAAC;CACJ;AA/ED,gCA+EC"}
1
+ {"version":3,"file":"YamlParser.js","sourceRoot":"","sources":["../../../src/core/YamlParser.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { LintReport } from '../types/Report';
2
2
  /**
3
- * Format lint report as a modern, interactive HTML page
3
+ * Format lint report as a modern, interactive HTML Single Page Application
4
4
  */
5
5
  export declare function formatHtml(report: LintReport): string;
6
6
  //# sourceMappingURL=HtmlFormatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HtmlFormatter.d.ts","sourceRoot":"","sources":["../../../src/formatters/HtmlFormatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAkXrD"}
1
+ {"version":3,"file":"HtmlFormatter.d.ts","sourceRoot":"","sources":["../../../src/formatters/HtmlFormatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7C;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CA6mBrD"}
@@ -1,386 +1,631 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.formatHtml = formatHtml;
7
+ const rules_1 = require("../rules");
8
+ const package_json_1 = __importDefault(require("../../package.json"));
4
9
  /**
5
- * Format lint report as a modern, interactive HTML page
10
+ * Format lint report as a modern, interactive HTML Single Page Application
6
11
  */
7
12
  function formatHtml(report) {
8
- const title = 'Mule-Lint Report';
9
- const date = new Date(report.timestamp).toLocaleString();
10
- // Calculate stats
11
- const totalErrors = report.summary.bySeverity.error;
12
- const totalWarnings = report.summary.bySeverity.warning;
13
- const totalInfos = report.summary.bySeverity.info;
14
- const totalIssues = totalErrors + totalWarnings + totalInfos;
15
- // Collect all issues into a flat list for the table
16
- const allIssues = [];
17
- for (const file of report.files) {
18
- if (!file.parsed) {
19
- allIssues.push({
20
- severity: 'error',
21
- file: file.relativePath,
22
- line: 1,
23
- column: 1,
24
- message: file.parseError || 'Failed to parse XML file',
25
- ruleId: 'PARSE-ERROR'
26
- });
27
- continue;
28
- }
29
- for (const issue of file.issues) {
30
- allIssues.push({
31
- severity: issue.severity,
32
- file: file.relativePath,
33
- line: issue.line,
34
- column: issue.column || 0,
35
- message: issue.message,
36
- ruleId: issue.ruleId
37
- });
38
- }
39
- }
13
+ // 1. Enrich Data
14
+ const enrichedFiles = report.files.map(file => ({
15
+ ...file,
16
+ issues: file.issues.map(issue => {
17
+ const ruleDef = rules_1.ALL_RULES.find(r => r.id === issue.ruleId);
18
+ return {
19
+ ...issue,
20
+ category: ruleDef?.category || 'General',
21
+ ruleDescription: ruleDef?.description || 'No description available',
22
+ ruleName: ruleDef?.name || issue.ruleId,
23
+ file: file.relativePath
24
+ };
25
+ })
26
+ }));
27
+ // Reconstruct report data for the client
28
+ const clientData = {
29
+ metadata: {
30
+ timestamp: report.timestamp,
31
+ version: package_json_1.default.version,
32
+ filesScanned: report.files.length,
33
+ duration: report.durationMs || 0
34
+ },
35
+ summary: report.summary,
36
+ files: enrichedFiles,
37
+ rules: rules_1.ALL_RULES.map(r => ({ id: r.id, name: r.name, category: r.category, severity: r.severity }))
38
+ };
39
+ const jsonPayload = JSON.stringify(clientData).replace(/</g, '\\u003c');
40
40
  return `<!DOCTYPE html>
41
- <html lang="en">
41
+ <html lang="en" class="bg-gray-50 text-slate-800">
42
42
  <head>
43
43
  <meta charset="UTF-8">
44
44
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
45
- <title>${title}</title>
46
- <style>
47
- :root {
48
- --primary: #00A1DF; /* MuleSoft Blue */
49
- --primary-dark: #0077A5;
50
- --success: #4CAF50;
51
- --error: #F44336;
52
- --warning: #FF9800;
53
- --info: #2196F3;
54
- --surface: #ffffff;
55
- --background: #f4f6f8;
56
- --text-primary: #172b4d;
57
- --text-secondary: #6b778c;
58
- --border: #dfe1e6;
59
- --shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
45
+ <title>Mule-Lint Report</title>
46
+
47
+ <!-- Fonts -->
48
+ <link rel="preconnect" href="https://fonts.googleapis.com">
49
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
51
+
52
+ <!-- Tailwind CSS -->
53
+ <script src="https://cdn.tailwindcss.com"></script>
54
+ <script>
55
+ tailwind.config = {
56
+ theme: {
57
+ extend: {
58
+ fontFamily: {
59
+ sans: ['Inter', 'sans-serif'],
60
+ mono: ['JetBrains Mono', 'monospace'],
61
+ },
62
+ colors: {
63
+ brand: {
64
+ 50: '#f0f9ff',
65
+ 100: '#e0f2fe',
66
+ 500: '#0ea5e9',
67
+ 600: '#0284c7',
68
+ 900: '#0c4a6e',
69
+ },
70
+ severity: {
71
+ error: '#ef4444',
72
+ warning: '#f59e0b',
73
+ info: '#3b82f6'
74
+ }
75
+ }
76
+ }
77
+ }
60
78
  }
79
+ </script>
80
+
81
+ <!-- Icons -->
82
+ <script src="https://unpkg.com/lucide@latest"></script>
61
83
 
62
- body {
63
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
64
- margin: 0;
65
- padding: 0;
66
- background-color: var(--background);
67
- color: var(--text-primary);
68
- line-height: 1.6;
69
- }
84
+ <!-- Chart.js -->
85
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
86
+
87
+ <!-- Tabulator -->
88
+ <link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
89
+ <script type="text/javascript" src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
70
90
 
71
- /* Layout */
72
- .container {
73
- width: 95%; /* Full width as requested */
74
- margin: 0 auto;
75
- padding: 20px;
76
- }
91
+ <style>
92
+ /* Custom Scrollbar */
93
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
94
+ ::-webkit-scrollbar-track { background: transparent; }
95
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
96
+ ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
77
97
 
78
- /* Header */
79
- header {
80
- background-color: var(--surface);
81
- padding: 1rem 0;
82
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
83
- margin-bottom: 2rem;
84
- position: sticky;
85
- top: 0;
86
- z-index: 100;
87
- }
88
- .header-content {
89
- display: flex;
90
- justify-content: space-between;
91
- align-items: center;
98
+ /* Tabulator Overrides */
99
+ .tabulator {
100
+ border: none !important;
101
+ background-color: transparent !important;
102
+ font-family: 'Inter', sans-serif !important;
92
103
  }
93
- h1 { margin: 0; color: var(--primary); font-size: 1.5rem; display: flex; align-items: center; gap: 10px; }
94
- .meta { color: var(--text-secondary); font-size: 0.9em; }
95
-
96
- /* Dashboard Grid */
97
- .dashboard {
98
- display: grid;
99
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
100
- gap: 20px;
101
- margin-bottom: 30px;
104
+ .tabulator-header {
105
+ background-color: #f8fafc !important;
106
+ border-bottom: 2px solid #e2e8f0 !important;
107
+ color: #64748b !important;
108
+ font-weight: 600 !important;
109
+ font-size: 0.75rem !important;
110
+ text-transform: uppercase !important;
111
+ letter-spacing: 0.05em !important;
102
112
  }
103
-
104
- /* Cards */
105
- .card {
106
- background: var(--surface);
107
- padding: 24px;
108
- border-radius: 8px;
109
- box-shadow: var(--shadow);
110
- display: flex;
111
- flex-direction: column;
112
- align-items: center;
113
- justify-content: center;
114
- transition: transform 0.2s;
113
+ .tabulator-header .tabulator-col {
114
+ background-color: #f8fafc !important;
115
+ border-right: none !important;
115
116
  }
116
- .card:hover { transform: translateY(-2px); }
117
-
118
- .number { font-size: 3rem; font-weight: 700; line-height: 1; margin-bottom: 0.5rem; }
119
- .label { color: var(--text-secondary); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
120
-
121
- /* Filters */
122
- .controls {
123
- display: flex;
124
- gap: 15px;
125
- margin-bottom: 20px;
126
- align-items: center;
127
- background: var(--surface);
128
- padding: 15px;
129
- border-radius: 8px;
130
- box-shadow: var(--shadow);
117
+ .tabulator-row {
118
+ background-color: white !important;
119
+ border-bottom: 1px solid #f1f5f9 !important;
120
+ color: #334155 !important;
131
121
  }
132
- .search-box {
133
- flex: 1;
134
- padding: 10px 15px;
135
- border: 1px solid var(--border);
136
- border-radius: 6px;
137
- font-size: 1rem;
122
+ .tabulator-row:hover {
123
+ background-color: #f8fafc !important;
138
124
  }
139
- .filter-group {
140
- display: flex;
141
- gap: 10px;
125
+ .tabulator-row.tabulator-selected {
126
+ background-color: #e0f2fe !important;
142
127
  }
143
- .filter-btn {
144
- padding: 8px 16px;
145
- border: 1px solid var(--border);
146
- background: var(--background);
147
- border-radius: 6px;
148
- cursor: pointer;
149
- font-weight: 500;
150
- color: var(--text-secondary);
151
- display: flex;
152
- align-items: center;
153
- gap: 6px;
128
+ .tabulator-cell {
129
+ padding: 14px 20px !important;
130
+ border-right: none !important;
131
+ font-size: 0.9rem !important;
154
132
  }
155
- .filter-btn.active {
156
- background: var(--primary);
157
- color: white;
158
- border-color: var(--primary);
133
+
134
+ /* Header Filter Inputs */
135
+ .tabulator-header-filter input,
136
+ .tabulator-header-filter select {
137
+ width: 100% !important;
138
+ padding: 6px 10px !important;
139
+ border: 2px solid #cbd5e1 !important;
140
+ border-radius: 6px !important;
141
+ font-size: 0.8rem !important;
142
+ background-color: #ffffff !important;
143
+ color: #334155 !important;
144
+ outline: none !important;
159
145
  }
160
- .filter-btn:hover:not(.active) { background: #e0e0e0; }
161
-
162
- /* Issues Table */
163
- .table-container {
164
- background: var(--surface);
165
- border-radius: 8px;
166
- box-shadow: var(--shadow);
167
- overflow-x: auto;
146
+ .tabulator-header-filter input:focus,
147
+ .tabulator-header-filter select:focus {
148
+ border-color: #0ea5e9 !important;
149
+ box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15) !important;
168
150
  }
169
- table {
170
- width: 100%;
171
- border-collapse: collapse;
151
+ .tabulator-header-filter input::placeholder {
152
+ color: #94a3b8 !important;
172
153
  }
173
154
 
174
- th, td {
175
- text-align: left;
176
- padding: 12px 20px;
177
- border-bottom: 1px solid var(--border);
155
+ /* Tabulator List Filter Styling */
156
+ .tabulator-edit-list {
157
+ background: white !important;
158
+ border: 2px solid #e2e8f0 !important;
159
+ border-radius: 8px !important;
160
+ box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1) !important;
161
+ max-height: 200px !important;
162
+ overflow-y: auto !important;
178
163
  }
179
- th {
180
- background: #f9f9f9;
181
- color: var(--text-secondary);
182
- font-weight: 600;
183
- font-size: 0.85rem;
184
- text-transform: uppercase;
185
- position: sticky;
186
- top: 0;
164
+ .tabulator-edit-list-item {
165
+ padding: 8px 12px !important;
166
+ font-size: 0.85rem !important;
167
+ color: #334155 !important;
187
168
  }
188
- tr:hover { background-color: #f5f9ff; }
189
- tr:last-child td { border-bottom: none; }
190
-
191
- .severity-badge {
192
- font-weight: 700;
193
- text-transform: uppercase;
194
- font-size: 0.75rem;
195
- padding: 4px 8px;
196
- border-radius: 4px;
197
- display: inline-block;
198
- min-width: 60px;
199
- text-align: center;
169
+ .tabulator-edit-list-item:hover {
170
+ background-color: #f1f5f9 !important;
200
171
  }
201
- .bg-error { background-color: #ffebee; color: var(--error); }
202
- .bg-warning { background-color: #fff3e0; color: var(--warning); }
203
- .bg-info { background-color: #e3f2fd; color: var(--info); }
204
-
205
- .file-link { font-family: monospace; font-weight: 600; color: var(--text-primary); }
206
- .location { font-family: monospace; color: var(--text-secondary); }
207
-
208
- .rule-pill {
209
- display: inline-block;
210
- background: #eef2f5;
211
- padding: 2px 8px;
212
- border-radius: 4px;
213
- font-size: 0.8rem;
214
- color: var(--text-secondary);
215
- font-family: monospace;
216
- border: 1px solid #dce1e6;
172
+ .tabulator-edit-list-item.active {
173
+ background-color: #0ea5e9 !important;
174
+ color: white !important;
217
175
  }
218
-
219
- .empty-state {
220
- text-align: center;
221
- padding: 60px;
222
- color: var(--text-secondary);
176
+
177
+ /* Chart Container */
178
+ .chart-container {
179
+ position: relative;
180
+ height: 250px;
181
+ width: 100%;
223
182
  }
224
- .empty-icon { font-size: 3rem; margin-bottom: 1rem; }
225
183
 
226
- /* Responsive */
227
- @media (max-width: 768px) {
228
- .controls { flex-direction: column; align-items: stretch; }
229
- .filter-group { justify-content: space-between; }
230
- }
184
+ [x-cloak] { display: none !important; }
231
185
  </style>
232
186
  </head>
233
187
  <body>
234
- <header>
235
- <div class="container header-content">
236
- <div>
237
- <h1>
238
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
239
- ${title}
240
- </h1>
241
- <div class="meta">Generated on ${date}</div>
242
- </div>
243
- <div>
244
- <a href="#" onclick="window.print()" style="color: var(--primary); text-decoration: none; font-weight: 500;">Download / Print</a>
245
- </div>
246
- </div>
247
- </header>
248
188
 
249
- <div class="container">
250
- <!-- Dashboard -->
251
- <div class="dashboard">
252
- <div class="card">
253
- <span class="number" style="color: var(--error)">${totalErrors}</span>
254
- <span class="label">Errors</span>
255
- </div>
256
- <div class="card">
257
- <span class="number" style="color: var(--warning)">${totalWarnings}</span>
258
- <span class="label">Warnings</span>
259
- </div>
260
- <div class="card">
261
- <span class="number" style="color: var(--info)">${totalInfos}</span>
262
- <span class="label">Infos</span>
263
- </div>
264
- <div class="card">
265
- <span class="number">${report.files.length}</span>
266
- <span class="label">Files Scanned</span>
267
- </div>
268
- </div>
189
+ <!-- Data Injection -->
190
+ <script id="report-data" type="application/json">
191
+ ${jsonPayload}
192
+ </script>
193
+
194
+ <div id="app" class="min-h-screen flex flex-col">
195
+
196
+ <!-- Navbar -->
197
+ <header class="bg-white border-b border-gray-200 sticky top-0 z-50">
198
+ <div class="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8">
199
+ <div class="flex justify-between h-16">
200
+ <div class="flex items-center gap-4">
201
+ <div class="flex items-center gap-2">
202
+ <div class="bg-brand-600 text-white p-1.5 rounded-lg shadow-sm">
203
+ <i data-lucide="shield-check" class="w-6 h-6"></i>
204
+ </div>
205
+ <div>
206
+ <h1 class="text-xl font-bold text-gray-900 tracking-tight">Mule-Lint</h1>
207
+ <p class="text-xs text-gray-500 font-medium">Enterprise Static Analysis</p>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Tabs -->
212
+ <nav class="ml-10 flex space-x-1 bg-gray-100 p-1 rounded-lg">
213
+ <button onclick="router.navigate('dashboard')"
214
+ id="nav-dashboard"
215
+ data-active="true"
216
+ class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200 text-gray-600 hover:text-gray-900 data-[active=true]:bg-white data-[active=true]:text-brand-600 data-[active=true]:shadow-sm">
217
+ Dashboard
218
+ </button>
219
+ <button onclick="router.navigate('issues')"
220
+ id="nav-issues"
221
+ class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200 text-gray-600 hover:text-gray-900 data-[active=true]:bg-white data-[active=true]:text-brand-600 data-[active=true]:shadow-sm">
222
+ Issues
223
+ <span id="nav-badge" class="ml-2 bg-gray-200 text-gray-600 px-1.5 py-0.5 rounded-full text-xs">0</span>
224
+ </button>
225
+ </nav>
226
+ </div>
269
227
 
270
- <!-- Controls -->
271
- <div class="controls">
272
- <input type="text" id="searchInput" class="search-box" placeholder="Search by file, message, or rule..." onkeyup="filterTable()">
273
- <div class="filter-group">
274
- <button class="filter-btn active" onclick="toggleFilter('all', this)" id="btn-all">All</button>
275
- <button class="filter-btn" onclick="toggleFilter('error', this)" id="btn-error">Errors</button>
276
- <button class="filter-btn" onclick="toggleFilter('warning', this)" id="btn-warning">Warnings</button>
277
- <button class="filter-btn" onclick="toggleFilter('info', this)" id="btn-info">Infos</button>
228
+ <div class="flex items-center gap-4">
229
+ <span class="text-sm text-gray-500 hidden md:block">
230
+ Generated <span id="timestamp-display" class="font-mono text-gray-700"></span>
231
+ </span>
232
+ <div class="h-6 w-px bg-gray-200"></div>
233
+ <a href="https://github.com/mulesoft-labs/mule-lint" target="_blank" class="text-gray-500 hover:text-gray-900 transition-colors">
234
+ <i data-lucide="github" class="w-5 h-5"></i>
235
+ </a>
236
+ <button onclick="window.print()" class="text-gray-500 hover:text-brand-600 transition-colors" title="Print Report">
237
+ <i data-lucide="printer" class="w-5 h-5"></i>
238
+ </button>
239
+ </div>
240
+ </div>
278
241
  </div>
279
- </div>
242
+ </header>
280
243
 
281
- <!-- Main Table -->
282
- <div class="table-container">
283
- <table id="issuesTable">
284
- <thead>
285
- <tr>
286
- <th width="100">Severity</th>
287
- <th width="150">Rule</th>
288
- <th width="300">File</th>
289
- <th width="100">Location</th>
290
- <th>Message</th>
291
- </tr>
292
- </thead>
293
- <tbody>
294
- ${allIssues.map(issue => renderIssueRow(issue)).join('')}
295
- </tbody>
296
- </table>
244
+ <!-- Main Content -->
245
+ <main class="flex-1 w-full">
297
246
 
298
- ${totalIssues === 0 && report.summary.parseErrors === 0 ? `
299
- <div class="empty-state">
300
- <div class="empty-icon">🎉</div>
301
- <h2>No issues found!</h2>
302
- <p>Your MuleSoft code looks clean and compliant.</p>
247
+ <!-- Dashboard View -->
248
+ <div id="view-dashboard" class="space-y-6 max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 py-8">
249
+ <!-- Summary Cards -->
250
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
251
+ <!-- Error Card -->
252
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-start justify-between cursor-pointer hover:shadow-md transition-shadow group" onclick="router.filterBySeverity('error')">
253
+ <div>
254
+ <p class="text-sm font-medium text-gray-500">Errors</p>
255
+ <h3 class="text-3xl font-bold text-severity-error mt-2" id="stat-error">0</h3>
256
+ <p class="text-xs text-gray-400 mt-1 group-hover:text-red-400 transition-colors">Critical violations</p>
257
+ </div>
258
+ <div class="p-3 bg-red-50 rounded-lg group-hover:bg-red-100 transition-colors">
259
+ <i data-lucide="x-circle" class="w-6 h-6 text-severity-error"></i>
260
+ </div>
261
+ </div>
262
+
263
+ <!-- Warning Card -->
264
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-start justify-between cursor-pointer hover:shadow-md transition-shadow group" onclick="router.filterBySeverity('warning')">
265
+ <div>
266
+ <p class="text-sm font-medium text-gray-500">Warnings</p>
267
+ <h3 class="text-3xl font-bold text-severity-warning mt-2" id="stat-warning">0</h3>
268
+ <p class="text-xs text-gray-400 mt-1 group-hover:text-orange-400 transition-colors">Potential issues</p>
269
+ </div>
270
+ <div class="p-3 bg-orange-50 rounded-lg group-hover:bg-orange-100 transition-colors">
271
+ <i data-lucide="alert-triangle" class="w-6 h-6 text-severity-warning"></i>
272
+ </div>
273
+ </div>
274
+
275
+ <!-- Info Card -->
276
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-start justify-between cursor-pointer hover:shadow-md transition-shadow group" onclick="router.filterBySeverity('info')">
277
+ <div>
278
+ <p class="text-sm font-medium text-gray-500">Infos</p>
279
+ <h3 class="text-3xl font-bold text-severity-info mt-2" id="stat-info">0</h3>
280
+ <p class="text-xs text-gray-400 mt-1 group-hover:text-blue-400 transition-colors">Suggestions</p>
281
+ </div>
282
+ <div class="p-3 bg-blue-50 rounded-lg group-hover:bg-blue-100 transition-colors">
283
+ <i data-lucide="info" class="w-6 h-6 text-severity-info"></i>
284
+ </div>
285
+ </div>
286
+
287
+ <!-- Files Card -->
288
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-start justify-between">
289
+ <div>
290
+ <p class="text-sm font-medium text-gray-500">Files Scanned</p>
291
+ <h3 class="text-3xl font-bold text-gray-700 mt-2" id="stat-files">0</h3>
292
+ <p class="text-xs text-gray-400 mt-1">XML Resources</p>
293
+ </div>
294
+ <div class="p-3 bg-gray-50 rounded-lg">
295
+ <i data-lucide="file-code" class="w-6 h-6 text-gray-500"></i>
296
+ </div>
297
+ </div>
298
+ </div>
299
+
300
+ <!-- Charts Section -->
301
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
302
+ <!-- Top Rules -->
303
+ <div class="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
304
+ <div class="flex items-center justify-between mb-6">
305
+ <div>
306
+ <h3 class="text-lg font-semibold text-gray-800">Top 5 Violated Rules</h3>
307
+ <p class="text-sm text-gray-500 mt-1">Most frequently triggered lint rules</p>
308
+ </div>
309
+ </div>
310
+ <div class="chart-container">
311
+ <canvas id="chart-top-rules"></canvas>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- Severity Distribution -->
316
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
317
+ <div class="flex items-center justify-between mb-6">
318
+ <div>
319
+ <h3 class="text-lg font-semibold text-gray-800">Severity Breakdown</h3>
320
+ <p class="text-sm text-gray-500 mt-1">Distribution by issue type</p>
321
+ </div>
322
+ </div>
323
+ <div class="chart-container flex items-center justify-center">
324
+ <canvas id="chart-severity"></canvas>
325
+ </div>
326
+ </div>
327
+ </div>
328
+
329
+ <!-- Categories Section -->
330
+ <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
331
+ <div class="flex items-center justify-between mb-6">
332
+ <div>
333
+ <h3 class="text-lg font-semibold text-gray-800">Issues by Category</h3>
334
+ <p class="text-sm text-gray-500 mt-1">Grouped by rule category</p>
335
+ </div>
336
+ <button onclick="router.navigate('issues')" class="text-sm text-brand-600 hover:text-brand-900 font-medium">View All Issues &rarr;</button>
337
+ </div>
338
+ <div class="h-[300px] w-full relative">
339
+ <canvas id="chart-categories"></canvas>
340
+ </div>
341
+ </div>
303
342
  </div>
304
- ` : ''}
305
-
306
- <div id="noResults" class="empty-state" style="display: none;">
307
- <h2>No matching issues found</h2>
308
- <p>Try adjusting your search filters.</p>
343
+
344
+ <!-- Issues View -->
345
+ <div id="view-issues" class="hidden h-[calc(100vh-80px)] flex flex-col px-6 py-4 overflow-hidden">
346
+ <!-- Filters Toolbar -->
347
+ <div class="bg-white p-4 rounded-t-xl border border-gray-200 border-b-0 shadow-sm flex flex-wrap gap-4 items-center justify-between flex-shrink-0">
348
+ <div class="flex items-center gap-4 flex-1">
349
+ <div class="relative flex-1 max-w-md">
350
+ <i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
351
+ <input type="text" id="global-search" placeholder="Quick search..."
352
+ class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent transition-shadow">
353
+ </div>
354
+ <div class="text-sm text-gray-500 italic">
355
+ <i data-lucide="filter" class="w-4 h-4 inline mr-1"></i>
356
+ Use column headers to filter (multiselect supported)
357
+ </div>
358
+ </div>
359
+
360
+ <div class="flex items-center gap-2">
361
+ <button id="download-csv" class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-300 rounded-lg hover:bg-gray-100 transition-colors">
362
+ <i data-lucide="download" class="w-4 h-4"></i>
363
+ Export CSV
364
+ </button>
365
+ </div>
366
+ </div>
367
+
368
+ <!-- Tabulator Container - uses remaining height -->
369
+ <div id="issues-table-container" class="flex-1 bg-white border border-gray-200 rounded-b-xl overflow-hidden shadow-sm"></div>
309
370
  </div>
310
- </div>
371
+ </main>
311
372
  </div>
312
373
 
313
374
  <script>
314
- let currentSeverity = 'all';
375
+ // --- State ---
376
+ const reportRaw = document.getElementById('report-data').textContent;
377
+ const report = JSON.parse(reportRaw);
378
+
379
+ // Flatten issues for the table
380
+ const allIssues = report.files.flatMap(f => f.issues.map(i => ({
381
+ ...i,
382
+ fileName: f.relativePath
383
+ })));
315
384
 
316
- function toggleFilter(severity, btn) {
317
- currentSeverity = severity;
318
-
319
- // Update buttons
320
- document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
321
- btn.classList.add('active');
385
+ // --- Router ---
386
+ const router = {
387
+ navigate(view) {
388
+ ['dashboard', 'issues'].forEach(v => {
389
+ document.getElementById(\`view-\${v}\`).classList.toggle('hidden', v !== view);
390
+ const btn = document.getElementById(\`nav-\${v}\`);
391
+ if (v === view) btn.setAttribute('data-active', 'true');
392
+ else btn.removeAttribute('data-active');
393
+ });
394
+ if (view === 'issues' && window.tableInstance) window.tableInstance.redraw();
395
+ },
322
396
 
323
- filterTable();
324
- }
397
+ filterBySeverity(severity) {
398
+ this.navigate('issues');
399
+ window.tableInstance.setHeaderFilterValue("severity", [severity]);
400
+ }
401
+ };
402
+
403
+ // --- Renderer ---
404
+ const renderer = {
405
+ init() {
406
+ document.getElementById('timestamp-display').textContent = new Date(report.metadata.timestamp).toLocaleString();
407
+
408
+ document.getElementById('stat-error').textContent = report.summary.bySeverity.error;
409
+ document.getElementById('stat-warning').textContent = report.summary.bySeverity.warning;
410
+ document.getElementById('stat-info').textContent = report.summary.bySeverity.info;
411
+ document.getElementById('stat-files').textContent = report.metadata.filesScanned;
412
+ document.getElementById('nav-badge').textContent = allIssues.length;
413
+
414
+ this.renderCharts();
415
+ this.initTable();
416
+ lucide.createIcons();
417
+ },
418
+
419
+ renderCharts() {
420
+ const colors = { error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' };
421
+
422
+ // Top Rules (Bar) - Using Names
423
+ const ruleCounts = {};
424
+ const ruleNames = {};
425
+ allIssues.forEach(i => {
426
+ ruleCounts[i.ruleId] = (ruleCounts[i.ruleId] || 0) + 1;
427
+ ruleNames[i.ruleId] = i.ruleName;
428
+ });
429
+
430
+ const sortedRules = Object.entries(ruleCounts)
431
+ .sort((a, b) => b[1] - a[1])
432
+ .slice(0, 5);
433
+
434
+ new Chart(document.getElementById('chart-top-rules'), {
435
+ type: 'bar',
436
+ data: {
437
+ labels: sortedRules.map(x => {
438
+ const id = x[0];
439
+ const name = ruleNames[id];
440
+ return name.length > 30 ? name.substring(0, 27) + '...' : name;
441
+ }),
442
+ datasets: [{
443
+ label: 'Violations',
444
+ data: sortedRules.map(x => x[1]),
445
+ backgroundColor: '#0ea5e9',
446
+ borderRadius: 6,
447
+ }]
448
+ },
449
+ options: {
450
+ maintainAspectRatio: false,
451
+ plugins: {
452
+ legend: { display: false },
453
+ tooltip: {
454
+ callbacks: {
455
+ title: (ctx) => {
456
+ const idx = ctx[0].dataIndex;
457
+ const id = sortedRules[idx][0];
458
+ return \`\${ruleNames[id]} (\${id})\`;
459
+ }
460
+ }
461
+ }
462
+ },
463
+ scales: { y: { beginAtZero: true, grid: { borderDash: [2, 4], color: '#f1f5f9' } }, x: { grid: { display: false } } }
464
+ }
465
+ });
325
466
 
326
- function filterTable() {
327
- const input = document.getElementById('searchInput');
328
- const filter = input.value.toLowerCase();
329
- const table = document.getElementById('issuesTable');
330
- const tr = table.getElementsByTagName('tr');
331
- let visibleCount = 0;
467
+ // Severity (Doughnut)
468
+ new Chart(document.getElementById('chart-severity'), {
469
+ type: 'doughnut',
470
+ data: {
471
+ labels: ['Error', 'Warning', 'Info'],
472
+ datasets: [{
473
+ data: [report.summary.bySeverity.error, report.summary.bySeverity.warning, report.summary.bySeverity.info],
474
+ backgroundColor: [colors.error, colors.warning, colors.info],
475
+ borderWidth: 0,
476
+ hoverOffset: 4
477
+ }]
478
+ },
479
+ options: {
480
+ cutout: '75%',
481
+ plugins: { legend: { position: 'bottom', labels: { usePointStyle: true, padding: 20 } } }
482
+ }
483
+ });
484
+
485
+ // Categories (Bar) - Colorful
486
+ const catCounts = {};
487
+ allIssues.forEach(i => catCounts[i.category] = (catCounts[i.category] || 0) + 1);
488
+ const sortedCats = Object.entries(catCounts).sort((a,b) => b[1] - a[1]);
489
+
490
+ // Color palette for categories
491
+ const categoryColors = [
492
+ '#0ea5e9', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444',
493
+ '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1'
494
+ ];
495
+
496
+ new Chart(document.getElementById('chart-categories'), {
497
+ type: 'bar',
498
+ data: {
499
+ labels: sortedCats.map(x => x[0].charAt(0).toUpperCase() + x[0].slice(1)),
500
+ datasets: [{
501
+ label: 'Issues',
502
+ data: sortedCats.map(x => x[1]),
503
+ backgroundColor: sortedCats.map((_, i) => categoryColors[i % categoryColors.length]),
504
+ borderRadius: 4,
505
+ barThickness: 20
506
+ }]
507
+ },
508
+ options: {
509
+ indexAxis: 'y',
510
+ maintainAspectRatio: false,
511
+ plugins: { legend: { display: false } },
512
+ scales: { x: { beginAtZero: true, grid: { borderDash: [2, 4], color: '#f1f5f9' } }, y: { grid: { display: false } } }
513
+ }
514
+ });
515
+ },
332
516
 
333
- for (let i = 1; i < tr.length; i++) {
334
- const row = tr[i];
335
- const severity = row.getAttribute('data-severity');
336
- const text = row.innerText.toLowerCase();
517
+ initTable() {
518
+ const table = new Tabulator("#issues-table-container", {
519
+ data: allIssues,
520
+ layout: "fitColumns",
521
+ height: "100%",
522
+ placeholder: "No issues found matching filters",
523
+ frozenRows: 0, // Header is always frozen in Tabulator by default
524
+
525
+ columns: [
526
+ {
527
+ title: "Severity",
528
+ field: "severity",
529
+ width: 140,
530
+ headerFilter: "list",
531
+ headerFilterParams: {
532
+ valuesLookup: true,
533
+ multiselect: true,
534
+ clearable: true
535
+ },
536
+ headerFilterFunc: "in",
537
+ formatter: (cell) => {
538
+ const val = cell.getValue();
539
+ const colorMap = {
540
+ error: "bg-red-100 text-red-700 border-red-200",
541
+ warning: "bg-orange-100 text-orange-700 border-orange-200",
542
+ info: "bg-blue-100 text-blue-700 border-blue-200"
543
+ };
544
+ return '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ' + (colorMap[val] || 'bg-gray-100') + ' uppercase tracking-wide">' + val + '</span>';
545
+ }
546
+ },
547
+ {
548
+ title: "Rule",
549
+ field: "ruleName",
550
+ width: 250,
551
+ headerFilter: "list",
552
+ headerFilterParams: {
553
+ valuesLookup: true,
554
+ multiselect: true,
555
+ clearable: true
556
+ },
557
+ headerFilterFunc: "in",
558
+ formatter: (cell) => {
559
+ const row = cell.getRow().getData();
560
+ return '<div class="flex flex-col"><span class="font-medium text-sm text-gray-900">' + cell.getValue() + '</span><span class="text-xs text-gray-500 font-mono">' + row.ruleId + '</span></div>';
561
+ }
562
+ },
563
+ {
564
+ title: "Category",
565
+ field: "category",
566
+ width: 140,
567
+ headerFilter: "list",
568
+ headerFilterParams: {
569
+ valuesLookup: true,
570
+ multiselect: true,
571
+ clearable: true
572
+ },
573
+ headerFilterFunc: "in",
574
+ formatter: (cell) => '<span class="text-sm text-gray-600 capitalize">' + cell.getValue() + '</span>'
575
+ },
576
+ {
577
+ title: "File Path",
578
+ field: "fileName",
579
+ headerFilter: "input",
580
+ headerFilterPlaceholder: "Filter...",
581
+ formatter: (cell) => {
582
+ const row = cell.getRow().getData();
583
+ return '<div class="flex flex-col"><span class="font-mono text-sm text-brand-600 font-medium truncate" title="' + row.fileName + '">' + row.fileName + '</span><span class="text-xs text-gray-400 font-mono">Line: ' + row.line + '</span></div>';
584
+ },
585
+ },
586
+ {
587
+ title: "Message",
588
+ field: "message",
589
+ widthGrow: 2,
590
+ headerFilter: "input",
591
+ headerFilterPlaceholder: "Filter...",
592
+ formatter: (cell) => {
593
+ const row = cell.getRow().getData();
594
+ return '<div class="flex flex-col gap-1"><span class="text-sm text-gray-700">' + cell.getValue() + '</span><span class="text-xs text-gray-400">' + row.ruleDescription + '</span></div>';
595
+ }
596
+ }
597
+ ],
598
+ initialSort: [{ column: "severity", dir: "asc" }]
599
+ });
337
600
 
338
- const severityMatch = currentSeverity === 'all' || severity === currentSeverity;
339
- const textMatch = text.includes(filter);
601
+ window.tableInstance = table;
340
602
 
341
- if (severityMatch && textMatch) {
342
- row.style.display = '';
343
- visibleCount++;
344
- } else {
345
- row.style.display = 'none';
346
- }
347
- }
348
-
349
- // Show/hide no results message
350
- const noResults = document.getElementById('noResults');
351
- const emptyState = document.querySelector('.empty-state:not(#noResults)');
352
-
353
- if (emptyState) return; // Don't interfere if main empty state is shown
354
-
355
- if (visibleCount === 0 && tr.length > 1) {
356
- noResults.style.display = 'block';
357
- table.style.display = 'none';
358
- } else {
359
- noResults.style.display = 'none';
360
- table.style.display = '';
603
+ // Quick Search
604
+ document.getElementById('global-search').addEventListener('keyup', (e) => {
605
+ const term = e.target.value.toLowerCase();
606
+ if (!term) { table.clearFilter(); return; }
607
+ table.setFilter((data) => (
608
+ data.message.toLowerCase().includes(term) ||
609
+ data.fileName.toLowerCase().includes(term) ||
610
+ data.ruleName.toLowerCase().includes(term) ||
611
+ data.ruleId.toLowerCase().includes(term)
612
+ ));
613
+ });
614
+
615
+ // Export
616
+ document.getElementById('download-csv').addEventListener('click', () => {
617
+ table.download("csv", "mule-lint-report.csv");
618
+ });
361
619
  }
362
- }
620
+ };
621
+
622
+ // --- Init ---
623
+ document.addEventListener('DOMContentLoaded', () => {
624
+ renderer.init();
625
+ });
626
+
363
627
  </script>
364
628
  </body>
365
629
  </html>`;
366
630
  }
367
- function renderIssueRow(issue) {
368
- const badgeClass = issue.severity === 'error' ? 'bg-error' :
369
- issue.severity === 'warning' ? 'bg-warning' : 'bg-info';
370
- return `<tr data-severity="${issue.severity}">
371
- <td><span class="severity-badge ${badgeClass}">${issue.severity}</span></td>
372
- <td><span class="rule-pill">${issue.ruleId}</span></td>
373
- <td class="file-link" title="${issue.file}">${issue.file}</td>
374
- <td class="location">${issue.line}:${issue.column}</td>
375
- <td>${escapeHtml(issue.message)}</td>
376
- </tr>`;
377
- }
378
- function escapeHtml(unsafe) {
379
- return unsafe
380
- .replace(/&/g, "&amp;")
381
- .replace(/</g, "&lt;")
382
- .replace(/>/g, "&gt;")
383
- .replace(/"/g, "&quot;")
384
- .replace(/'/g, "&#039;");
385
- }
386
631
  //# sourceMappingURL=HtmlFormatter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"HtmlFormatter.js","sourceRoot":"","sources":["../../../src/formatters/HtmlFormatter.ts"],"names":[],"mappings":";;AAMA,gCAkXC;AArXD;;GAEG;AACH,SAAgB,UAAU,CAAC,MAAkB;IACzC,MAAM,KAAK,GAAG,kBAAkB,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,CAAC;IAEzD,kBAAkB;IAClB,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC;IACpD,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;IACxD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;IAClD,MAAM,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC;IAE7D,oDAAoD;IACpD,MAAM,SAAS,GAOV,EAAE,CAAC;IAER,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACf,SAAS,CAAC,IAAI,CAAC;gBACX,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,IAAI,CAAC,YAAY;gBACvB,IAAI,EAAE,CAAC;gBACP,MAAM,EAAE,CAAC;gBACT,OAAO,EAAE,IAAI,CAAC,UAAU,IAAI,0BAA0B;gBACtD,MAAM,EAAE,aAAa;aACxB,CAAC,CAAC;YACH,SAAS;QACb,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9B,SAAS,CAAC,IAAI,CAAC;gBACX,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,IAAI,EAAE,IAAI,CAAC,YAAY;gBACvB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC;gBACzB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,MAAM,EAAE,KAAK,CAAC,MAAM;aACvB,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IAED,OAAO;;;;;aAKE,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAkMI,KAAK;;iDAEsB,IAAI;;;;;;;;;;;;mEAYc,WAAW;;;;qEAIT,aAAa;;;;kEAIhB,UAAU;;;;uCAIrC,MAAM,CAAC,KAAK,CAAC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBA6BpC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;;cAI9D,WAAW,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC;;;;;;aAMzD,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6DV,CAAC;AACT,CAAC;AAED,SAAS,cAAc,CAAC,KAAU;IAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC1C,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1E,OAAO,sBAAsB,KAAK,CAAC,QAAQ;0CACL,UAAU,KAAK,KAAK,CAAC,QAAQ;sCACjC,KAAK,CAAC,MAAM;uCACX,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI;+BACjC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM;cAC3C,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;UAC7B,CAAC;AACX,CAAC;AAED,SAAS,UAAU,CAAC,MAAc;IAC9B,OAAO,MAAM;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACjC,CAAC"}
1
+ {"version":3,"file":"HtmlFormatter.js","sourceRoot":"","sources":["../../../src/formatters/HtmlFormatter.ts"],"names":[],"mappings":";;;;;AAOA,gCA6mBC;AAnnBD,oCAAqC;AACrC,sEAA6C;AAE7C;;GAEG;AACH,SAAgB,UAAU,CAAC,MAAkB;IACzC,iBAAiB;IACjB,MAAM,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5C,GAAG,IAAI;QACP,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;YAC5B,MAAM,OAAO,GAAG,iBAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC;YAC3D,OAAO;gBACH,GAAG,KAAK;gBACR,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,SAAS;gBACxC,eAAe,EAAE,OAAO,EAAE,WAAW,IAAI,0BAA0B;gBACnE,QAAQ,EAAE,OAAO,EAAE,IAAI,IAAI,KAAK,CAAC,MAAM;gBACvC,IAAI,EAAE,IAAI,CAAC,YAAY;aAC1B,CAAC;QACN,CAAC,CAAC;KACL,CAAC,CAAC,CAAC;IAEJ,yCAAyC;IACzC,MAAM,UAAU,GAAG;QACf,QAAQ,EAAE;YACN,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,sBAAW,CAAC,OAAO;YAC5B,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;YACjC,QAAQ,EAAE,MAAM,CAAC,UAAU,IAAI,CAAC;SACnC;QACD,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,aAAa;QACpB,KAAK,EAAE,iBAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;KACtG,CAAC;IAEF,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAExE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuJD,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAsbb,CAAC;AACT,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sfdxy/mule-lint",
3
- "version": "1.5.2",
3
+ "version": "1.7.0",
4
4
  "description": "Static analysis tool for MuleSoft applications - supports humans, AI agents, and CI/CD pipelines",
5
5
  "author": "Avinava",
6
6
  "license": "MIT",