mysql_genius-core 0.5.1 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -3
- data/lib/mysql_genius/core/version.rb +1 -1
- data/mysql_genius-core.gemspec +1 -2
- metadata +2 -14
- data/lib/mysql_genius/core/views/mysql_genius/queries/_shared_results.html.erb +0 -56
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_ai_tools.html.erb +0 -43
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_dashboard.html.erb +0 -95
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +0 -35
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_explorer.html.erb +0 -110
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_stats.html.erb +0 -26
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_server.html.erb +0 -54
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_slow_queries.html.erb +0 -17
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_table_sizes.html.erb +0 -33
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_unused_indexes.html.erb +0 -36
- data/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb +0 -1565
|
@@ -1,1565 +0,0 @@
|
|
|
1
|
-
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
2
|
-
<h4>🐘 MySQLGenius</h4>
|
|
3
|
-
<button class="mg-theme-toggle" id="mg-theme-btn" title="Toggle dark/light theme" onclick="(function(){var d=document.documentElement,t=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',t);localStorage.setItem('mg-theme',t);document.getElementById('mg-theme-btn').textContent=t==='dark'?'\u2600\uFE0F':'\uD83C\uDF19';})()">
|
|
4
|
-
<script>document.write(document.documentElement.getAttribute('data-theme')==='dark'?'\u2600\uFE0F':'\uD83C\uDF19')</script>
|
|
5
|
-
</button>
|
|
6
|
-
</div>
|
|
7
|
-
|
|
8
|
-
<div class="mg-tabs">
|
|
9
|
-
<button class="mg-tab active" data-tab="dashboard">Dashboard</button>
|
|
10
|
-
<button class="mg-tab" data-tab="slow">Slow Queries</button>
|
|
11
|
-
<button class="mg-tab" data-tab="qstats">Query Stats</button>
|
|
12
|
-
<button class="mg-tab" data-tab="server">Server</button>
|
|
13
|
-
<button class="mg-tab" data-tab="tables">Tables</button>
|
|
14
|
-
<button class="mg-tab" data-tab="unused">Unused Indexes</button>
|
|
15
|
-
<button class="mg-tab" data-tab="indexes">Duplicate Indexes</button>
|
|
16
|
-
<button class="mg-tab" data-tab="explorer">Query Explorer</button>
|
|
17
|
-
<% if @ai_enabled %>
|
|
18
|
-
<button class="mg-tab" data-tab="aitools">AI Tools</button>
|
|
19
|
-
<% end %>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<%= render_partial(:tab_dashboard) %>
|
|
23
|
-
<%= render_partial(:tab_slow_queries) %>
|
|
24
|
-
<%= render_partial(:tab_query_stats) %>
|
|
25
|
-
<%= render_partial(:tab_server) %>
|
|
26
|
-
<%= render_partial(:tab_table_sizes) %>
|
|
27
|
-
<%= render_partial(:tab_unused_indexes) %>
|
|
28
|
-
<%= render_partial(:tab_duplicate_indexes) %>
|
|
29
|
-
<%= render_partial(:tab_query_explorer) %>
|
|
30
|
-
<% if @ai_enabled %>
|
|
31
|
-
<%= render_partial(:tab_ai_tools) %>
|
|
32
|
-
<% end %>
|
|
33
|
-
|
|
34
|
-
<%= render_partial(:shared_results) %>
|
|
35
|
-
|
|
36
|
-
<script>
|
|
37
|
-
(function() {
|
|
38
|
-
"use strict";
|
|
39
|
-
|
|
40
|
-
var ROUTES = {
|
|
41
|
-
columns: '<%= path_for(:columns) %>',
|
|
42
|
-
execute: '<%= path_for(:execute) %>',
|
|
43
|
-
explain: '<%= path_for(:explain) %>',
|
|
44
|
-
suggest: '<%= path_for(:suggest) %>',
|
|
45
|
-
optimize: '<%= path_for(:optimize) %>',
|
|
46
|
-
slow_queries: '<%= path_for(:slow_queries) %>',
|
|
47
|
-
duplicate_indexes: '<%= path_for(:duplicate_indexes) %>',
|
|
48
|
-
table_sizes: '<%= path_for(:table_sizes) %>',
|
|
49
|
-
query_stats: '<%= path_for(:query_stats) %>',
|
|
50
|
-
unused_indexes: '<%= path_for(:unused_indexes) %>',
|
|
51
|
-
server_overview: '<%= path_for(:server_overview) %>',
|
|
52
|
-
describe_query: '<%= path_for(:describe_query) %>',
|
|
53
|
-
schema_review: '<%= path_for(:schema_review) %>',
|
|
54
|
-
rewrite_query: '<%= path_for(:rewrite_query) %>',
|
|
55
|
-
index_advisor: '<%= path_for(:index_advisor) %>',
|
|
56
|
-
anomaly_detection: '<%= path_for(:anomaly_detection) %>',
|
|
57
|
-
root_cause: '<%= path_for(:root_cause) %>',
|
|
58
|
-
migration_risk: '<%= path_for(:migration_risk) %>'
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
var RAILS_MIGRATION_VERSION = '<%= @framework_version_major %>.<%= @framework_version_minor %>';
|
|
62
|
-
|
|
63
|
-
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
|
64
|
-
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
|
65
|
-
var currentColumns = [];
|
|
66
|
-
var columnTypeMap = {};
|
|
67
|
-
var lastExplainSql = '';
|
|
68
|
-
var lastExplainRows = [];
|
|
69
|
-
|
|
70
|
-
// --- Helpers ---
|
|
71
|
-
|
|
72
|
-
function el(id) { return document.getElementById(id); }
|
|
73
|
-
function qs(sel, parent) { return (parent || document).querySelector(sel); }
|
|
74
|
-
function qsa(sel, parent) { return Array.from((parent || document).querySelectorAll(sel)); }
|
|
75
|
-
function show(e) { e.classList.remove('mg-hidden'); }
|
|
76
|
-
function hide(e) { e.classList.add('mg-hidden'); }
|
|
77
|
-
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
78
|
-
|
|
79
|
-
// --- Table Sorting ---
|
|
80
|
-
|
|
81
|
-
function parseSortValue(text) {
|
|
82
|
-
var m = text.match(/([\d.]+)\s*(s|ms|KB|MB|GB)/i);
|
|
83
|
-
if (m) {
|
|
84
|
-
var n = parseFloat(m[1]);
|
|
85
|
-
var unit = m[2].toLowerCase();
|
|
86
|
-
if (unit === 's') return n * 1000;
|
|
87
|
-
if (unit === 'gb') return n * 1024;
|
|
88
|
-
if (unit === 'mb') return n;
|
|
89
|
-
if (unit === 'kb') return n / 1024;
|
|
90
|
-
return n;
|
|
91
|
-
}
|
|
92
|
-
return parseFloat(text.replace(/[^0-9.\-]/g, '')) || 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function makeSortable(table) {
|
|
96
|
-
if (table.dataset.sortable) return;
|
|
97
|
-
table.dataset.sortable = '1';
|
|
98
|
-
|
|
99
|
-
var headers = Array.from(table.querySelectorAll('th'));
|
|
100
|
-
headers.forEach(function(th, colIdx) {
|
|
101
|
-
if (th.dataset.noSort || th.textContent.trim() === '' || th.textContent.trim() === 'Actions') return;
|
|
102
|
-
th.classList.add('mg-sortable');
|
|
103
|
-
th.addEventListener('click', function() {
|
|
104
|
-
var tbody = table.querySelector('tbody');
|
|
105
|
-
if (!tbody) return;
|
|
106
|
-
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
107
|
-
if (rows.length === 0) return;
|
|
108
|
-
|
|
109
|
-
var asc = !th.classList.contains('mg-sort-asc');
|
|
110
|
-
headers.forEach(function(h) { h.classList.remove('mg-sort-asc', 'mg-sort-desc'); });
|
|
111
|
-
th.classList.add(asc ? 'mg-sort-asc' : 'mg-sort-desc');
|
|
112
|
-
|
|
113
|
-
var isNum = th.style.textAlign === 'right' || th.classList.contains('mg-num');
|
|
114
|
-
|
|
115
|
-
rows.sort(function(a, b) {
|
|
116
|
-
var cellA = a.children[colIdx];
|
|
117
|
-
var cellB = b.children[colIdx];
|
|
118
|
-
if (!cellA || !cellB) return 0;
|
|
119
|
-
var valA = cellA.textContent.trim();
|
|
120
|
-
var valB = cellB.textContent.trim();
|
|
121
|
-
|
|
122
|
-
if (isNum) {
|
|
123
|
-
return asc ? parseSortValue(valA) - parseSortValue(valB) : parseSortValue(valB) - parseSortValue(valA);
|
|
124
|
-
}
|
|
125
|
-
return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
rows.forEach(function(row) { tbody.appendChild(row); });
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// --- SQL Syntax Highlighting (single-pass tokenizer) ---
|
|
134
|
-
|
|
135
|
-
var SQL_KW_SET = {};
|
|
136
|
-
'SELECT FROM WHERE JOIN LEFT RIGHT INNER OUTER CROSS FULL ON AND OR NOT IN EXISTS BETWEEN LIKE IS NULL AS DISTINCT ALL UNION INTERSECT EXCEPT INSERT INTO VALUES UPDATE SET DELETE CREATE ALTER DROP TABLE INDEX VIEW TRIGGER PROCEDURE FUNCTION DATABASE SCHEMA IF THEN ELSE END CASE WHEN HAVING LIMIT OFFSET ASC DESC USING FORCE USE IGNORE WITH RECURSIVE OVER EXPLAIN ANALYZE SHOW DESCRIBE'.split(' ').forEach(function(w) { SQL_KW_SET[w] = true; });
|
|
137
|
-
|
|
138
|
-
var SQL_FN_SET = {};
|
|
139
|
-
'COUNT SUM AVG MIN MAX COALESCE IFNULL NULLIF CAST CONVERT CONCAT CONCAT_WS GROUP_CONCAT SUBSTRING SUBSTR REPLACE TRIM LTRIM RTRIM UPPER LOWER UCASE LCASE LENGTH CHAR_LENGTH NOW CURDATE CURTIME DATE_FORMAT DATE_ADD DATE_SUB DATEDIFF TIMESTAMPDIFF UNIX_TIMESTAMP FROM_UNIXTIME IF ELT FIELD FIND_IN_SET FORMAT HEX UNHEX MD5 SHA1 SHA2 AES_ENCRYPT AES_DECRYPT ROUND CEIL CEILING FLOOR ABS MOD POWER SQRT LOG LOG2 LOG10 RAND GREATEST LEAST JSON_EXTRACT JSON_UNQUOTE JSON_SET JSON_OBJECT JSON_ARRAY JSON_CONTAINS JSON_LENGTH ROW_NUMBER RANK DENSE_RANK LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE'.split(' ').forEach(function(w) { SQL_FN_SET[w] = true; });
|
|
140
|
-
|
|
141
|
-
// Single-pass tokenizer regex: matches tokens in order of priority
|
|
142
|
-
// 1) single-quoted strings 2) backtick identifiers 3) numbers 4) words 5) operators 6) punctuation 7) ? placeholder 8) * star 9) whitespace 10) anything else
|
|
143
|
-
var SQL_TOKEN_RE = /'[^']*'|`[^`]*`|\b\d+(?:\.\d+)?\b|[A-Za-z_]\w*|\?\|>=|<=|<>|!=|[><=]|[(),;*]|\s+|./g;
|
|
144
|
-
|
|
145
|
-
function highlightSql(sql) {
|
|
146
|
-
if (!sql) return '';
|
|
147
|
-
var result = [];
|
|
148
|
-
var match;
|
|
149
|
-
SQL_TOKEN_RE.lastIndex = 0;
|
|
150
|
-
while ((match = SQL_TOKEN_RE.exec(sql)) !== null) {
|
|
151
|
-
var tok = match[0];
|
|
152
|
-
var ch = tok.charAt(0);
|
|
153
|
-
|
|
154
|
-
if (ch === "'") {
|
|
155
|
-
// String literal
|
|
156
|
-
result.push('<span class="mg-sql-str">' + escHtml(tok) + '</span>');
|
|
157
|
-
} else if (ch === '`') {
|
|
158
|
-
// Backtick-quoted identifier
|
|
159
|
-
result.push('<span class="mg-sql-tbl">' + escHtml(tok) + '</span>');
|
|
160
|
-
} else if (/^\d/.test(tok)) {
|
|
161
|
-
// Number
|
|
162
|
-
result.push('<span class="mg-sql-num">' + escHtml(tok) + '</span>');
|
|
163
|
-
} else if (/^[A-Za-z_]/.test(tok)) {
|
|
164
|
-
// Word: check if keyword, function, or plain identifier
|
|
165
|
-
var upper = tok.toUpperCase();
|
|
166
|
-
// Check for compound keywords (GROUP BY, ORDER BY, PARTITION BY)
|
|
167
|
-
var rest = sql.substring(SQL_TOKEN_RE.lastIndex);
|
|
168
|
-
var compoundMatch = rest.match(/^(\s+)(BY)\b/i);
|
|
169
|
-
if ((upper === 'GROUP' || upper === 'ORDER' || upper === 'PARTITION') && compoundMatch) {
|
|
170
|
-
result.push('<span class="mg-sql-kw">' + escHtml(tok) + escHtml(compoundMatch[1]) + escHtml(compoundMatch[2].toUpperCase()) + '</span>');
|
|
171
|
-
SQL_TOKEN_RE.lastIndex += compoundMatch[0].length;
|
|
172
|
-
} else if (SQL_FN_SET[upper] && rest.match(/^\s*\(/)) {
|
|
173
|
-
result.push('<span class="mg-sql-fn">' + escHtml(tok.toUpperCase()) + '</span>');
|
|
174
|
-
} else if (SQL_KW_SET[upper]) {
|
|
175
|
-
result.push('<span class="mg-sql-kw">' + escHtml(tok.toUpperCase()) + '</span>');
|
|
176
|
-
} else {
|
|
177
|
-
result.push(escHtml(tok));
|
|
178
|
-
}
|
|
179
|
-
} else if (tok === '?') {
|
|
180
|
-
result.push('<span class="mg-sql-placeholder">?</span>');
|
|
181
|
-
} else if (tok === '*') {
|
|
182
|
-
result.push('<span class="mg-sql-star">*</span>');
|
|
183
|
-
} else if (/^(>=|<=|<>|!=|[><=])$/.test(tok)) {
|
|
184
|
-
result.push('<span class="mg-sql-op">' + escHtml(tok) + '</span>');
|
|
185
|
-
} else if (/^[(),;]$/.test(tok)) {
|
|
186
|
-
result.push('<span class="mg-sql-punc">' + escHtml(tok) + '</span>');
|
|
187
|
-
} else {
|
|
188
|
-
// Whitespace or unknown
|
|
189
|
-
result.push(escHtml(tok));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return result.join('');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function sqlBlock(sql, maxLen) {
|
|
196
|
-
var truncated = maxLen && sql.length > maxLen;
|
|
197
|
-
var displaySql = truncated ? sql.substring(0, maxLen) + '...' : sql;
|
|
198
|
-
return '<code class="mg-sql-block" title="' + escHtml(sql) + '">' + highlightSql(displaySql) + '</code>';
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// --- Duration Formatting with Color ---
|
|
202
|
-
|
|
203
|
-
function formatDurationStyled(ms) {
|
|
204
|
-
var text = formatDuration(ms);
|
|
205
|
-
var cls = 'mg-dur-fast';
|
|
206
|
-
if (ms >= 1000) cls = 'mg-dur-slow';
|
|
207
|
-
else if (ms >= 100) cls = 'mg-dur-moderate';
|
|
208
|
-
return '<span class="' + cls + '">' + text + '</span>';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function migrationTimestamp() {
|
|
212
|
-
var now = new Date();
|
|
213
|
-
return now.getFullYear().toString() +
|
|
214
|
-
('0' + (now.getMonth() + 1)).slice(-2) +
|
|
215
|
-
('0' + now.getDate()).slice(-2) +
|
|
216
|
-
('0' + now.getHours()).slice(-2) +
|
|
217
|
-
('0' + now.getMinutes()).slice(-2) +
|
|
218
|
-
('0' + now.getSeconds()).slice(-2);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function copyToClipboard(text, btn) {
|
|
222
|
-
var done = function() {
|
|
223
|
-
btn.innerHTML = '✓ Copied';
|
|
224
|
-
setTimeout(function() { btn.innerHTML = '📋 Copy'; }, 2000);
|
|
225
|
-
};
|
|
226
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
227
|
-
navigator.clipboard.writeText(text).then(done);
|
|
228
|
-
} else {
|
|
229
|
-
var ta = document.createElement('textarea');
|
|
230
|
-
ta.value = text;
|
|
231
|
-
ta.style.position = 'fixed';
|
|
232
|
-
ta.style.opacity = '0';
|
|
233
|
-
document.body.appendChild(ta);
|
|
234
|
-
ta.select();
|
|
235
|
-
document.execCommand('copy');
|
|
236
|
-
document.body.removeChild(ta);
|
|
237
|
-
done();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function ajax(method, url, data, onSuccess, onError) {
|
|
242
|
-
var xhr = new XMLHttpRequest();
|
|
243
|
-
xhr.open(method, url, true);
|
|
244
|
-
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
|
|
245
|
-
xhr.setRequestHeader('Accept', 'application/json');
|
|
246
|
-
xhr.onload = function() {
|
|
247
|
-
var json;
|
|
248
|
-
try { json = JSON.parse(xhr.responseText); } catch(e) { json = null; }
|
|
249
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
250
|
-
onSuccess(json);
|
|
251
|
-
} else {
|
|
252
|
-
(onError || function(){})(json, xhr.status);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
xhr.onerror = function() { (onError || function(){})(null, 0); };
|
|
256
|
-
if (data && method !== 'GET') {
|
|
257
|
-
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
258
|
-
var parts = [];
|
|
259
|
-
for (var k in data) {
|
|
260
|
-
if (Array.isArray(data[k])) {
|
|
261
|
-
data[k].forEach(function(v, i) {
|
|
262
|
-
if (typeof v === 'object') {
|
|
263
|
-
for (var vk in v) parts.push(encodeURIComponent(k + '[' + i + '][' + vk + ']') + '=' + encodeURIComponent(v[vk]));
|
|
264
|
-
} else {
|
|
265
|
-
parts.push(encodeURIComponent(k + '[]') + '=' + encodeURIComponent(v));
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
} else {
|
|
269
|
-
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
xhr.send(parts.join('&'));
|
|
273
|
-
} else if (method === 'GET' && data) {
|
|
274
|
-
xhr.send();
|
|
275
|
-
} else {
|
|
276
|
-
xhr.send();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function ajaxGet(url, params, onSuccess, onError) {
|
|
281
|
-
var query = Object.keys(params).map(function(k) {
|
|
282
|
-
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
|
|
283
|
-
}).join('&');
|
|
284
|
-
ajax('GET', url + '?' + query, null, onSuccess, onError);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// --- Tabs ---
|
|
288
|
-
|
|
289
|
-
function activateTab(name) {
|
|
290
|
-
qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
291
|
-
qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
292
|
-
var btn = qs('[data-tab="' + name + '"]');
|
|
293
|
-
if (!btn) { name = 'dashboard'; btn = qs('[data-tab="dashboard"]'); }
|
|
294
|
-
btn.classList.add('active');
|
|
295
|
-
el('tab-' + name).classList.add('active');
|
|
296
|
-
history.replaceState(null, '', location.pathname + '#' + name);
|
|
297
|
-
if (name === 'dashboard') loadDashboard();
|
|
298
|
-
if (name === 'slow') loadSlowQueries();
|
|
299
|
-
if (name === 'indexes') loadDuplicateIndexes();
|
|
300
|
-
if (name === 'tables') loadTableSizes();
|
|
301
|
-
if (name === 'qstats') loadQueryStats();
|
|
302
|
-
if (name === 'unused') loadUnusedIndexes();
|
|
303
|
-
if (name === 'server') loadServerOverview();
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
qsa('.mg-tab').forEach(function(tab) {
|
|
307
|
-
tab.addEventListener('click', function() { activateTab(tab.dataset.tab); });
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// --- Dashboard ---
|
|
311
|
-
|
|
312
|
-
function loadDashboard() {
|
|
313
|
-
show(el('dash-loading'));
|
|
314
|
-
hide(el('dash-content')); hide(el('dash-error'));
|
|
315
|
-
// Reset previous data
|
|
316
|
-
hide(el('dash-slow-table')); hide(el('dash-slow-empty'));
|
|
317
|
-
hide(el('dash-qstats-table')); hide(el('dash-qstats-empty')); hide(el('dash-qstats-error'));
|
|
318
|
-
el('dash-slow-tbody').innerHTML = '';
|
|
319
|
-
el('dash-qstats-tbody').innerHTML = '';
|
|
320
|
-
el('dash-dup-count').textContent = '--';
|
|
321
|
-
el('dash-unused-count').textContent = '--';
|
|
322
|
-
|
|
323
|
-
var loaded = { server: false, slow: false, qstats: false, dup: false, unused: false };
|
|
324
|
-
|
|
325
|
-
function checkAllLoaded() {
|
|
326
|
-
if (!loaded.server || !loaded.slow || !loaded.qstats || !loaded.dup || !loaded.unused) return;
|
|
327
|
-
hide(el('dash-loading'));
|
|
328
|
-
show(el('dash-content'));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Server overview
|
|
332
|
-
ajaxGet(ROUTES.server_overview, {}, function(data) {
|
|
333
|
-
if (data.error) { loaded.server = true; checkAllLoaded(); return; }
|
|
334
|
-
var s = data.server;
|
|
335
|
-
var c = data.connections;
|
|
336
|
-
var db = data.innodb;
|
|
337
|
-
var q = data.queries;
|
|
338
|
-
|
|
339
|
-
el('dash-server-info').innerHTML =
|
|
340
|
-
statRow('Version', '<code>' + escHtml(s.version) + '</code>') +
|
|
341
|
-
statRow('Uptime', escHtml(s.uptime)) +
|
|
342
|
-
statRow('Queries/sec', q.qps);
|
|
343
|
-
|
|
344
|
-
el('dash-conn-bar').innerHTML = usageBar(c.usage_pct, c.current + ' / ' + c.max + ' (' + c.usage_pct + '%)');
|
|
345
|
-
el('dash-conn-info').innerHTML =
|
|
346
|
-
statRow('Threads Running', c.threads_running) +
|
|
347
|
-
statRow('Max Used', c.max_used);
|
|
348
|
-
|
|
349
|
-
var poolUsedPct = db.buffer_pool_pages_total > 0
|
|
350
|
-
? (((db.buffer_pool_pages_total - db.buffer_pool_pages_free) / db.buffer_pool_pages_total) * 100).toFixed(1)
|
|
351
|
-
: 0;
|
|
352
|
-
el('dash-innodb-bar').innerHTML = usageBar(parseFloat(poolUsedPct), db.buffer_pool_mb + ' MB (' + poolUsedPct + '% used)');
|
|
353
|
-
el('dash-innodb-info').innerHTML =
|
|
354
|
-
statRow('Hit Rate', db.buffer_pool_hit_rate + '%') +
|
|
355
|
-
statRow('Dirty Pages', Number(db.buffer_pool_pages_dirty).toLocaleString());
|
|
356
|
-
|
|
357
|
-
var tmpBadge = q.tmp_disk_pct > 25
|
|
358
|
-
? '<span class="mg-badge mg-badge-danger">' + q.tmp_disk_pct + '%</span>'
|
|
359
|
-
: q.tmp_disk_pct + '%';
|
|
360
|
-
el('dash-query-info').innerHTML =
|
|
361
|
-
statRow('Slow Queries', Number(q.slow_queries).toLocaleString()) +
|
|
362
|
-
statRow('Tmp Disk Tables', tmpBadge);
|
|
363
|
-
|
|
364
|
-
loaded.server = true;
|
|
365
|
-
checkAllLoaded();
|
|
366
|
-
}, function() { loaded.server = true; checkAllLoaded(); });
|
|
367
|
-
|
|
368
|
-
// Top 5 slow queries
|
|
369
|
-
ajaxGet(ROUTES.slow_queries, {}, function(data) {
|
|
370
|
-
if (!data || !data.length) {
|
|
371
|
-
el('dash-slow-empty').textContent = 'No slow queries recorded.';
|
|
372
|
-
show(el('dash-slow-empty'));
|
|
373
|
-
} else {
|
|
374
|
-
var top5 = data.slice(0, 5);
|
|
375
|
-
el('dash-slow-tbody').innerHTML = top5.map(function(q) {
|
|
376
|
-
var d = q.duration_ms;
|
|
377
|
-
var cls = d >= 2000 ? 'mg-badge-danger' : d >= 1000 ? 'mg-badge-warning' : 'mg-badge-info';
|
|
378
|
-
return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
|
|
379
|
-
'<td><small>' + escHtml(q.timestamp) + '</small></td>' +
|
|
380
|
-
'<td>' + sqlBlock(q.sql, 120) + '</td></tr>';
|
|
381
|
-
}).join('');
|
|
382
|
-
show(el('dash-slow-table'));
|
|
383
|
-
}
|
|
384
|
-
loaded.slow = true;
|
|
385
|
-
checkAllLoaded();
|
|
386
|
-
}, function() {
|
|
387
|
-
el('dash-slow-empty').textContent = 'Configure redis_url to monitor slow queries.';
|
|
388
|
-
show(el('dash-slow-empty'));
|
|
389
|
-
loaded.slow = true;
|
|
390
|
-
checkAllLoaded();
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
// Top 5 expensive queries
|
|
394
|
-
ajaxGet(ROUTES.query_stats, { sort: 'total_time', limit: 5 }, function(data) {
|
|
395
|
-
if (data.error) {
|
|
396
|
-
el('dash-qstats-error').innerHTML = '<div class="mg-text-muted">' + escHtml(data.error) + '</div>';
|
|
397
|
-
show(el('dash-qstats-error'));
|
|
398
|
-
} else if (!data.length) {
|
|
399
|
-
el('dash-qstats-empty').textContent = 'No query statistics available.';
|
|
400
|
-
show(el('dash-qstats-empty'));
|
|
401
|
-
} else {
|
|
402
|
-
el('dash-qstats-tbody').innerHTML = data.map(function(q) {
|
|
403
|
-
return '<tr>' +
|
|
404
|
-
'<td>' + sqlBlock(q.sql, 120) + '</td>' +
|
|
405
|
-
'<td class="mg-num">' + Number(q.calls).toLocaleString() + '</td>' +
|
|
406
|
-
'<td class="mg-num">' + formatDurationStyled(q.total_time_ms) + '</td>' +
|
|
407
|
-
'<td class="mg-num">' + formatDurationStyled(q.avg_time_ms) + '</td></tr>';
|
|
408
|
-
}).join('');
|
|
409
|
-
show(el('dash-qstats-table'));
|
|
410
|
-
}
|
|
411
|
-
loaded.qstats = true;
|
|
412
|
-
checkAllLoaded();
|
|
413
|
-
}, function() {
|
|
414
|
-
el('dash-qstats-empty').textContent = 'Query statistics unavailable.';
|
|
415
|
-
show(el('dash-qstats-empty'));
|
|
416
|
-
loaded.qstats = true;
|
|
417
|
-
checkAllLoaded();
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Duplicate indexes count
|
|
421
|
-
ajaxGet(ROUTES.duplicate_indexes, {}, function(data) {
|
|
422
|
-
var count = Array.isArray(data) ? data.length : 0;
|
|
423
|
-
var countEl = el('dash-dup-count');
|
|
424
|
-
if (count === 0) {
|
|
425
|
-
countEl.innerHTML = '<span style="color:#28a745;">✓ 0</span>';
|
|
426
|
-
} else {
|
|
427
|
-
countEl.innerHTML = '<span style="color:#dc3545;">' + count + '</span>';
|
|
428
|
-
}
|
|
429
|
-
loaded.dup = true;
|
|
430
|
-
checkAllLoaded();
|
|
431
|
-
}, function() { el('dash-dup-count').textContent = '?'; loaded.dup = true; checkAllLoaded(); });
|
|
432
|
-
|
|
433
|
-
// Unused indexes count
|
|
434
|
-
ajaxGet(ROUTES.unused_indexes, {}, function(data) {
|
|
435
|
-
var count = Array.isArray(data) ? data.length : 0;
|
|
436
|
-
var countEl = el('dash-unused-count');
|
|
437
|
-
if (count === 0) {
|
|
438
|
-
countEl.innerHTML = '<span style="color:#28a745;">✓ 0</span>';
|
|
439
|
-
} else {
|
|
440
|
-
countEl.innerHTML = '<span style="color:#dc3545;">' + count + '</span>';
|
|
441
|
-
}
|
|
442
|
-
loaded.unused = true;
|
|
443
|
-
checkAllLoaded();
|
|
444
|
-
}, function() { el('dash-unused-count').textContent = '?'; loaded.unused = true; checkAllLoaded(); });
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Dashboard tab-jump buttons
|
|
448
|
-
document.addEventListener('click', function(e) {
|
|
449
|
-
var jumpBtn = e.target.closest('.dash-jump-tab');
|
|
450
|
-
if (jumpBtn) { activateTab(jumpBtn.dataset.target); }
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// Restore active tab from URL hash, or default to dashboard
|
|
454
|
-
var initialTab = (location.hash || '').replace('#', '') || 'dashboard';
|
|
455
|
-
activateTab(initialTab);
|
|
456
|
-
|
|
457
|
-
// --- Query Explorer Mode Toggle ---
|
|
458
|
-
|
|
459
|
-
qsa('.qe-mode').forEach(function(btn) {
|
|
460
|
-
btn.addEventListener('click', function() {
|
|
461
|
-
qsa('.qe-mode').forEach(function(b) {
|
|
462
|
-
b.classList.remove('active');
|
|
463
|
-
b.classList.add('mg-btn-outline');
|
|
464
|
-
});
|
|
465
|
-
btn.classList.add('active');
|
|
466
|
-
btn.classList.remove('mg-btn-outline');
|
|
467
|
-
if (btn.dataset.mode === 'visual') {
|
|
468
|
-
show(el('qe-visual'));
|
|
469
|
-
hide(el('qe-sql'));
|
|
470
|
-
} else {
|
|
471
|
-
hide(el('qe-visual'));
|
|
472
|
-
show(el('qe-sql'));
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
// --- Visual Builder ---
|
|
478
|
-
|
|
479
|
-
var typeLabels = { string: 'text', text: 'text', integer: 'number', boolean: 'yes/no', date: 'date', datetime: 'date/time', decimal: 'decimal', float: 'decimal' };
|
|
480
|
-
function colTypeLabel(type) { return typeLabels[type] || type; }
|
|
481
|
-
|
|
482
|
-
function operatorsForType(type) {
|
|
483
|
-
switch(type) {
|
|
484
|
-
case 'boolean': return ['=', '!=', 'IS NULL', 'IS NOT NULL'];
|
|
485
|
-
case 'date': case 'datetime': return ['=', '!=', '>', '<', '>=', '<=', 'BETWEEN', 'IS NULL', 'IS NOT NULL'];
|
|
486
|
-
case 'integer': case 'decimal': case 'float': return ['=', '!=', '>', '<', '>=', '<=', 'IS NULL', 'IS NOT NULL'];
|
|
487
|
-
default: return ['=', '!=', 'LIKE', '>', '<', 'IS NULL', 'IS NOT NULL'];
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function loadColumnsForTable(table, callback) {
|
|
492
|
-
el('vb-columns').innerHTML = '';
|
|
493
|
-
el('vb-filters').innerHTML = '';
|
|
494
|
-
el('vb-orders').innerHTML = '';
|
|
495
|
-
columnTypeMap = {};
|
|
496
|
-
if (!table) {
|
|
497
|
-
hide(el('vb-columns-section')); hide(el('vb-filters-section'));
|
|
498
|
-
hide(el('vb-order-section')); hide(el('vb-generated-sql'));
|
|
499
|
-
el('vb-run').disabled = true; el('vb-explain').disabled = true;
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
ajaxGet(ROUTES.columns, { table: table }, function(cols) {
|
|
503
|
-
currentColumns = cols;
|
|
504
|
-
cols.forEach(function(c) { columnTypeMap[c.name] = c.type; });
|
|
505
|
-
var html = '';
|
|
506
|
-
cols.forEach(function(col) {
|
|
507
|
-
var checked = col['default'] ? ' checked' : '';
|
|
508
|
-
html += '<label class="mg-check"><input type="checkbox" class="vb-col-check" value="' + escHtml(col.name) + '"' + checked +
|
|
509
|
-
' data-default="' + (col['default'] ? '1' : '0') + '">' + escHtml(col.name) +
|
|
510
|
-
' <span class="type-hint">(' + colTypeLabel(col.type) + ')</span></label>';
|
|
511
|
-
});
|
|
512
|
-
el('vb-columns').innerHTML = html;
|
|
513
|
-
show(el('vb-columns-section')); show(el('vb-filters-section'));
|
|
514
|
-
show(el('vb-order-section')); show(el('vb-generated-sql'));
|
|
515
|
-
el('vb-run').disabled = false; el('vb-explain').disabled = false;
|
|
516
|
-
if (callback) callback(cols); else updateGeneratedSql();
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
el('vb-table').addEventListener('change', function() { loadColumnsForTable(this.value); });
|
|
521
|
-
|
|
522
|
-
el('vb-toggle-all').addEventListener('click', function() {
|
|
523
|
-
var checks = qsa('.vb-col-check');
|
|
524
|
-
var allChecked = checks.every(function(c) { return c.checked; });
|
|
525
|
-
checks.forEach(function(c) { c.checked = !allChecked; });
|
|
526
|
-
updateGeneratedSql();
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
el('vb-show-defaults').addEventListener('click', function() {
|
|
530
|
-
qsa('.vb-col-check').forEach(function(c) { c.checked = c.dataset['default'] === '1'; });
|
|
531
|
-
updateGeneratedSql();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
document.addEventListener('change', function(e) {
|
|
535
|
-
if (e.target.classList.contains('vb-col-check')) updateGeneratedSql();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
function columnOptions(useAll) {
|
|
539
|
-
var cols = useAll ? currentColumns : currentColumns.filter(function(c) { return c['default']; });
|
|
540
|
-
var html = '<option value="">-- column --</option>';
|
|
541
|
-
cols.forEach(function(c) { html += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + ' (' + colTypeLabel(c.type) + ')</option>'; });
|
|
542
|
-
return html;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function addFilterRow(useAll) {
|
|
546
|
-
var div = document.createElement('div');
|
|
547
|
-
div.className = 'mg-row mg-mb vb-filter-row';
|
|
548
|
-
div.style.alignItems = 'center';
|
|
549
|
-
div.innerHTML =
|
|
550
|
-
'<div class="mg-col-3"><select class="vb-filter-col">' + columnOptions(useAll) + '</select></div>' +
|
|
551
|
-
'<div class="mg-col-2"><select class="vb-filter-op"><option>=</option><option>!=</option><option>LIKE</option><option>></option><option><</option><option>IS NULL</option><option>IS NOT NULL</option></select></div>' +
|
|
552
|
-
'<div class="mg-col-4 vb-filter-val-container"><input type="text" class="vb-filter-val" placeholder="value"></div>' +
|
|
553
|
-
'<div><button class="mg-btn mg-btn-outline-danger mg-btn-sm vb-remove-filter">✕</button></div>';
|
|
554
|
-
el('vb-filters').appendChild(div);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
el('vb-add-filter').addEventListener('click', function() { addFilterRow(false); updateGeneratedSql(); });
|
|
558
|
-
|
|
559
|
-
document.addEventListener('change', function(e) {
|
|
560
|
-
if (e.target.classList.contains('vb-filter-col')) {
|
|
561
|
-
var row = e.target.closest('.vb-filter-row');
|
|
562
|
-
var colType = columnTypeMap[e.target.value] || 'string';
|
|
563
|
-
var opSel = qs('.vb-filter-op', row);
|
|
564
|
-
var ops = operatorsForType(colType);
|
|
565
|
-
opSel.innerHTML = ops.map(function(o) { return '<option>' + o + '</option>'; }).join('');
|
|
566
|
-
updateValueInput(row, colType, opSel.value);
|
|
567
|
-
updateGeneratedSql();
|
|
568
|
-
}
|
|
569
|
-
if (e.target.classList.contains('vb-filter-op')) {
|
|
570
|
-
var row = e.target.closest('.vb-filter-row');
|
|
571
|
-
var colName = qs('.vb-filter-col', row).value;
|
|
572
|
-
var colType = columnTypeMap[colName] || 'string';
|
|
573
|
-
updateValueInput(row, colType, e.target.value);
|
|
574
|
-
updateGeneratedSql();
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
document.addEventListener('input', function(e) {
|
|
579
|
-
if (e.target.classList.contains('vb-filter-val')) updateGeneratedSql();
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
document.addEventListener('click', function(e) {
|
|
583
|
-
if (e.target.closest('.vb-remove-filter')) { e.target.closest('.vb-filter-row').remove(); updateGeneratedSql(); }
|
|
584
|
-
if (e.target.closest('.vb-remove-order')) { e.target.closest('.vb-order-row').remove(); updateGeneratedSql(); }
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
function updateValueInput(row, colType, op) {
|
|
588
|
-
var container = qs('.vb-filter-val-container', row);
|
|
589
|
-
if (op === 'IS NULL' || op === 'IS NOT NULL') { container.innerHTML = ''; return; }
|
|
590
|
-
if (colType === 'boolean') {
|
|
591
|
-
container.innerHTML = '<select class="vb-filter-val"><option value="1">True</option><option value="0">False</option></select>';
|
|
592
|
-
} else if ((colType === 'date' || colType === 'datetime') && op === 'BETWEEN') {
|
|
593
|
-
container.innerHTML = '<input type="date" class="vb-filter-val" style="width:45%;display:inline-block;"> <span class="mg-text-muted">and</span> <input type="date" class="vb-filter-val-end" style="width:45%;display:inline-block;">';
|
|
594
|
-
} else if (colType === 'date' || colType === 'datetime') {
|
|
595
|
-
container.innerHTML = '<input type="date" class="vb-filter-val">';
|
|
596
|
-
} else if (colType === 'integer') {
|
|
597
|
-
container.innerHTML = '<input type="number" class="vb-filter-val" placeholder="number" step="1">';
|
|
598
|
-
} else if (colType === 'decimal' || colType === 'float') {
|
|
599
|
-
container.innerHTML = '<input type="number" class="vb-filter-val" placeholder="number" step="any">';
|
|
600
|
-
} else {
|
|
601
|
-
var ph = (op === 'LIKE') ? 'use % as wildcard' : 'value';
|
|
602
|
-
container.innerHTML = '<input type="text" class="vb-filter-val" placeholder="' + ph + '">';
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// --- Order By ---
|
|
607
|
-
|
|
608
|
-
function addOrderRow(useAll) {
|
|
609
|
-
var cols = useAll ? currentColumns : currentColumns.filter(function(c) { return c['default']; });
|
|
610
|
-
var options = '<option value="">-- column --</option>';
|
|
611
|
-
cols.forEach(function(c) { options += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + '</option>'; });
|
|
612
|
-
var div = document.createElement('div');
|
|
613
|
-
div.className = 'mg-row mg-mb vb-order-row';
|
|
614
|
-
div.style.alignItems = 'center';
|
|
615
|
-
div.innerHTML =
|
|
616
|
-
'<div class="mg-col-3"><select class="vb-order-col">' + options + '</select></div>' +
|
|
617
|
-
'<div class="mg-col-2"><select class="vb-order-dir"><option value="ASC">Ascending</option><option value="DESC">Descending</option></select></div>' +
|
|
618
|
-
'<div><button class="mg-btn mg-btn-outline-danger mg-btn-sm vb-remove-order">✕</button></div>';
|
|
619
|
-
el('vb-orders').appendChild(div);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
el('vb-add-order').addEventListener('click', function() { addOrderRow(false); updateGeneratedSql(); });
|
|
623
|
-
|
|
624
|
-
document.addEventListener('change', function(e) {
|
|
625
|
-
if (e.target.classList.contains('vb-order-col') || e.target.classList.contains('vb-order-dir')) updateGeneratedSql();
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// --- Build SQL ---
|
|
629
|
-
|
|
630
|
-
function buildSql() {
|
|
631
|
-
var table = el('vb-table').value;
|
|
632
|
-
if (!table) return '';
|
|
633
|
-
var cols = [];
|
|
634
|
-
qsa('.vb-col-check:checked').forEach(function(c) { cols.push('`' + c.value + '`'); });
|
|
635
|
-
if (!cols.length) cols = ['*'];
|
|
636
|
-
var sql = 'SELECT ' + cols.join(', ') + ' FROM `' + table + '`';
|
|
637
|
-
var wheres = [];
|
|
638
|
-
qsa('.vb-filter-row').forEach(function(row) {
|
|
639
|
-
var col = qs('.vb-filter-col', row).value;
|
|
640
|
-
var op = qs('.vb-filter-op', row).value;
|
|
641
|
-
var valEl = qs('.vb-filter-val', row);
|
|
642
|
-
var val = valEl ? valEl.value : '';
|
|
643
|
-
if (!col) return;
|
|
644
|
-
if (op === 'IS NULL' || op === 'IS NOT NULL') {
|
|
645
|
-
wheres.push('`' + col + '` ' + op);
|
|
646
|
-
} else if (op === 'BETWEEN') {
|
|
647
|
-
var endEl = qs('.vb-filter-val-end', row);
|
|
648
|
-
var endVal = endEl ? endEl.value : '';
|
|
649
|
-
wheres.push("`" + col + "` BETWEEN '" + val.replace(/'/g, "''") + "' AND '" + endVal.replace(/'/g, "''") + "'");
|
|
650
|
-
} else if (op === 'LIKE') {
|
|
651
|
-
wheres.push("`" + col + "` LIKE '" + val.replace(/'/g, "''") + "'");
|
|
652
|
-
} else {
|
|
653
|
-
wheres.push("`" + col + "` " + op + " '" + val.replace(/'/g, "''") + "'");
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
if (wheres.length) sql += ' WHERE ' + wheres.join(' AND ');
|
|
657
|
-
var orders = [];
|
|
658
|
-
qsa('.vb-order-row').forEach(function(row) {
|
|
659
|
-
var col = qs('.vb-order-col', row).value;
|
|
660
|
-
var dir = qs('.vb-order-dir', row).value;
|
|
661
|
-
if (col) orders.push('`' + col + '` ' + dir);
|
|
662
|
-
});
|
|
663
|
-
if (orders.length) sql += ' ORDER BY ' + orders.join(', ');
|
|
664
|
-
return sql;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function updateGeneratedSql() {
|
|
668
|
-
var sql = buildSql();
|
|
669
|
-
el('vb-sql-preview').value = sql;
|
|
670
|
-
el('sql-input').value = sql;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// --- Run Query ---
|
|
674
|
-
|
|
675
|
-
el('vb-run').addEventListener('click', function() {
|
|
676
|
-
var sql = buildSql();
|
|
677
|
-
if (sql) runQuery(sql, parseInt(el('vb-row-limit').value) || 25);
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
el('sql-run').addEventListener('click', function() {
|
|
681
|
-
var sql = el('sql-input').value.trim();
|
|
682
|
-
if (sql) runQuery(sql, parseInt(el('sql-row-limit').value) || 25);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
el('sql-input').addEventListener('input', function() { el('vb-sql-preview').value = this.value; });
|
|
686
|
-
|
|
687
|
-
function runQuery(sql, rowLimit) {
|
|
688
|
-
clearResults();
|
|
689
|
-
setBtnLoading(['vb-run', 'sql-run'], true);
|
|
690
|
-
ajax('POST', ROUTES.execute, { sql: sql, row_limit: rowLimit }, function(data) {
|
|
691
|
-
renderResults(data);
|
|
692
|
-
setBtnLoading(['vb-run', 'sql-run'], false);
|
|
693
|
-
}, function(json) {
|
|
694
|
-
var cls = (json && json.timeout) ? 'mg-alert-warning' : 'mg-alert-danger';
|
|
695
|
-
el('results-alert').innerHTML = '<div class="mg-alert ' + cls + '">' + escHtml((json && json.error) || 'An unexpected error occurred.') + '</div>';
|
|
696
|
-
show(el('results-alert'));
|
|
697
|
-
setBtnLoading(['vb-run', 'sql-run'], false);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function clearResults() {
|
|
702
|
-
hide(el('results-alert')); hide(el('results-stats'));
|
|
703
|
-
hide(el('results-table-wrapper')); hide(el('results-empty'));
|
|
704
|
-
hide(el('results-truncated'));
|
|
705
|
-
el('results-thead').innerHTML = '';
|
|
706
|
-
el('results-tbody').innerHTML = '';
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function renderResults(data) {
|
|
710
|
-
if (data.row_count === 0) {
|
|
711
|
-
show(el('results-empty')); show(el('results-stats'));
|
|
712
|
-
el('results-row-count').textContent = '0 rows';
|
|
713
|
-
el('results-time').textContent = data.execution_time_ms + ' ms';
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
el('results-row-count').textContent = data.row_count + ' row' + (data.row_count !== 1 ? 's' : '');
|
|
717
|
-
el('results-time').textContent = data.execution_time_ms + ' ms';
|
|
718
|
-
if (data.truncated) show(el('results-truncated'));
|
|
719
|
-
show(el('results-stats'));
|
|
720
|
-
|
|
721
|
-
el('results-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
|
|
722
|
-
el('results-tbody').innerHTML = data.rows.map(function(row) {
|
|
723
|
-
return '<tr>' + row.map(function(val) {
|
|
724
|
-
if (val === null) return '<td><em class="null">NULL</em></td>';
|
|
725
|
-
if (val === '[REDACTED]') return '<td><span class="redacted">[REDACTED]</span></td>';
|
|
726
|
-
return '<td>' + escHtml(String(val)) + '</td>';
|
|
727
|
-
}).join('') + '</tr>';
|
|
728
|
-
}).join('');
|
|
729
|
-
show(el('results-table-wrapper'));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// --- Explain ---
|
|
733
|
-
|
|
734
|
-
el('vb-explain').addEventListener('click', function() { var sql = buildSql(); if (sql) runExplain(sql); });
|
|
735
|
-
el('sql-explain').addEventListener('click', function() { var sql = el('sql-input').value.trim(); if (sql) runExplain(sql); });
|
|
736
|
-
el('explain-close').addEventListener('click', function() { hide(el('explain-results')); });
|
|
737
|
-
|
|
738
|
-
function runExplain(sql, fromSlowQuery) {
|
|
739
|
-
lastExplainSql = sql;
|
|
740
|
-
hide(el('explain-results')); hide(el('optimize-results'));
|
|
741
|
-
el('explain-thead').innerHTML = '';
|
|
742
|
-
el('explain-tbody').innerHTML = '';
|
|
743
|
-
setBtnLoading(['vb-explain', 'sql-explain'], true);
|
|
744
|
-
|
|
745
|
-
var postData = { sql: sql };
|
|
746
|
-
if (fromSlowQuery) postData.from_slow_query = 'true';
|
|
747
|
-
ajax('POST', ROUTES.explain, postData, function(data) {
|
|
748
|
-
lastExplainRows = data.rows;
|
|
749
|
-
el('explain-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
|
|
750
|
-
el('explain-tbody').innerHTML = data.rows.map(function(row) {
|
|
751
|
-
return '<tr>' + row.map(function(val) {
|
|
752
|
-
return val === null ? '<td><em class="null">NULL</em></td>' : '<td>' + escHtml(String(val)) + '</td>';
|
|
753
|
-
}).join('') + '</tr>';
|
|
754
|
-
}).join('');
|
|
755
|
-
show(el('explain-results'));
|
|
756
|
-
setBtnLoading(['vb-explain', 'sql-explain'], false);
|
|
757
|
-
}, function(json) {
|
|
758
|
-
el('results-alert').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml((json && json.error) || 'Explain failed.') + '</div>';
|
|
759
|
-
show(el('results-alert'));
|
|
760
|
-
setBtnLoading(['vb-explain', 'sql-explain'], false);
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// --- AI ---
|
|
765
|
-
|
|
766
|
-
var aiToggle = el('ai-toggle');
|
|
767
|
-
if (aiToggle) {
|
|
768
|
-
aiToggle.addEventListener('click', function() {
|
|
769
|
-
var panel = el('ai-panel');
|
|
770
|
-
panel.classList.toggle('mg-hidden');
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
var aiSuggest = el('ai-suggest');
|
|
775
|
-
if (aiSuggest) {
|
|
776
|
-
aiSuggest.addEventListener('click', function() {
|
|
777
|
-
var prompt = el('ai-prompt').value.trim();
|
|
778
|
-
if (!prompt) return;
|
|
779
|
-
aiSuggest.disabled = true;
|
|
780
|
-
aiSuggest.innerHTML = '<span class="mg-spinner"></span> Thinking...';
|
|
781
|
-
hide(el('ai-result'));
|
|
782
|
-
|
|
783
|
-
ajax('POST', ROUTES.suggest, { prompt: prompt }, function(data) {
|
|
784
|
-
el('sql-input').value = data.sql || '';
|
|
785
|
-
if (data.explanation) {
|
|
786
|
-
el('ai-explanation').textContent = data.explanation;
|
|
787
|
-
show(el('ai-result'));
|
|
788
|
-
}
|
|
789
|
-
parseSqlToBuilder(data.sql);
|
|
790
|
-
aiSuggest.disabled = false;
|
|
791
|
-
aiSuggest.innerHTML = '⚡ Suggest Query';
|
|
792
|
-
}, function(json) {
|
|
793
|
-
el('results-alert').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml((json && json.error) || 'AI suggestion failed.') + '</div>';
|
|
794
|
-
show(el('results-alert'));
|
|
795
|
-
aiSuggest.disabled = false;
|
|
796
|
-
aiSuggest.innerHTML = '⚡ Suggest Query';
|
|
797
|
-
});
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
var explainOptimize = el('explain-optimize');
|
|
802
|
-
if (explainOptimize) {
|
|
803
|
-
explainOptimize.addEventListener('click', function() {
|
|
804
|
-
explainOptimize.disabled = true;
|
|
805
|
-
explainOptimize.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
806
|
-
hide(el('optimize-results'));
|
|
807
|
-
|
|
808
|
-
var data = { sql: lastExplainSql };
|
|
809
|
-
lastExplainRows.forEach(function(row, i) {
|
|
810
|
-
row.forEach(function(val, j) {
|
|
811
|
-
data['explain_rows[' + i + '][' + j + ']'] = val == null ? '' : val;
|
|
812
|
-
});
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
ajax('POST', ROUTES.optimize, data, function(resp) {
|
|
816
|
-
el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.');
|
|
817
|
-
show(el('optimize-results'));
|
|
818
|
-
explainOptimize.disabled = false;
|
|
819
|
-
explainOptimize.innerHTML = '⚡ AI Optimization';
|
|
820
|
-
}, function(json) {
|
|
821
|
-
el('optimize-content').textContent = (json && json.error) || 'Optimization failed.';
|
|
822
|
-
show(el('optimize-results'));
|
|
823
|
-
explainOptimize.disabled = false;
|
|
824
|
-
explainOptimize.innerHTML = '⚡ AI Optimization';
|
|
825
|
-
});
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// --- Slow Queries ---
|
|
830
|
-
|
|
831
|
-
function loadSlowQueries() {
|
|
832
|
-
show(el('slow-loading'));
|
|
833
|
-
hide(el('slow-empty')); hide(el('slow-table-wrapper'));
|
|
834
|
-
el('slow-tbody').innerHTML = '';
|
|
835
|
-
|
|
836
|
-
ajaxGet(ROUTES.slow_queries, {}, function(data) {
|
|
837
|
-
hide(el('slow-loading'));
|
|
838
|
-
if (!data || !data.length) { show(el('slow-empty')); el('slow-count').textContent = '0'; return; }
|
|
839
|
-
el('slow-count').textContent = data.length + ' queries';
|
|
840
|
-
el('slow-tbody').innerHTML = data.map(function(q) {
|
|
841
|
-
var d = q.duration_ms;
|
|
842
|
-
var cls = d >= 2000 ? 'mg-badge-danger' : d >= 1000 ? 'mg-badge-warning' : 'mg-badge-info';
|
|
843
|
-
return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
|
|
844
|
-
'<td><small>' + escHtml(q.timestamp) + '</small></td>' +
|
|
845
|
-
'<td>' + sqlBlock(q.sql, 200) + '</td>' +
|
|
846
|
-
'<td><button class="mg-btn mg-btn-outline mg-btn-sm slow-explain-btn" data-sql="' + escHtml(q.sql).replace(/"/g, '"') + '">Explain</button> ' +
|
|
847
|
-
'<button class="mg-btn mg-btn-outline-secondary mg-btn-sm slow-use-btn" data-sql="' + escHtml(q.sql).replace(/"/g, '"') + '">Use</button></td></tr>';
|
|
848
|
-
}).join('');
|
|
849
|
-
show(el('slow-table-wrapper'));
|
|
850
|
-
makeSortable(qs('#slow-table-wrapper .mg-table'));
|
|
851
|
-
}, function() {
|
|
852
|
-
hide(el('slow-loading'));
|
|
853
|
-
el('slow-empty').textContent = 'Failed to load slow queries.';
|
|
854
|
-
show(el('slow-empty'));
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
el('slow-refresh').addEventListener('click', loadSlowQueries);
|
|
859
|
-
|
|
860
|
-
document.addEventListener('click', function(e) {
|
|
861
|
-
var btn = e.target.closest('.slow-explain-btn');
|
|
862
|
-
if (btn) runExplain(btn.dataset.sql, true);
|
|
863
|
-
var useBtn = e.target.closest('.slow-use-btn');
|
|
864
|
-
if (useBtn) {
|
|
865
|
-
el('sql-input').value = useBtn.dataset.sql;
|
|
866
|
-
activateTab('explorer');
|
|
867
|
-
// Switch to SQL mode within Query Explorer
|
|
868
|
-
qsa('.qe-mode').forEach(function(b) { b.classList.remove('active'); b.classList.add('mg-btn-outline'); });
|
|
869
|
-
qs('.qe-mode[data-mode="sql"]').classList.add('active');
|
|
870
|
-
qs('.qe-mode[data-mode="sql"]').classList.remove('mg-btn-outline');
|
|
871
|
-
hide(el('qe-visual'));
|
|
872
|
-
show(el('qe-sql'));
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// --- Duplicate Indexes ---
|
|
877
|
-
|
|
878
|
-
function loadDuplicateIndexes() {
|
|
879
|
-
show(el('dup-loading'));
|
|
880
|
-
hide(el('dup-empty')); hide(el('dup-table-wrapper'));
|
|
881
|
-
el('dup-tbody').innerHTML = '';
|
|
882
|
-
|
|
883
|
-
ajaxGet(ROUTES.duplicate_indexes, {}, function(data) {
|
|
884
|
-
hide(el('dup-loading'));
|
|
885
|
-
if (!data || !data.length) { show(el('dup-empty')); el('dup-count').textContent = '0'; return; }
|
|
886
|
-
el('dup-count').textContent = data.length + ' found';
|
|
887
|
-
el('dup-tbody').innerHTML = data.map(function(d) {
|
|
888
|
-
var dropSql = 'ALTER TABLE `' + d.table + '` DROP INDEX `' + d.duplicate_index + '`;';
|
|
889
|
-
return '<tr>' +
|
|
890
|
-
'<td><strong>' + escHtml(d.table) + '</strong></td>' +
|
|
891
|
-
'<td><code>' + escHtml(d.duplicate_index) + '</code>' + (d.unique ? ' <span class="mg-badge mg-badge-warning">UNIQUE</span>' : '') + '</td>' +
|
|
892
|
-
'<td>' + d.duplicate_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
|
|
893
|
-
'<td><code>' + escHtml(d.covered_by_index) + '</code></td>' +
|
|
894
|
-
'<td>' + d.covered_by_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
|
|
895
|
-
'<td>' + sqlBlock(dropSql, 0) + '</td>' +
|
|
896
|
-
'</tr>';
|
|
897
|
-
}).join('');
|
|
898
|
-
show(el('dup-table-wrapper'));
|
|
899
|
-
makeSortable(qs('#dup-table-wrapper .mg-table'));
|
|
900
|
-
|
|
901
|
-
// Generate migration
|
|
902
|
-
var ts = migrationTimestamp();
|
|
903
|
-
var migrationLines = ['# ' + ts + '_remove_duplicate_indexes.rb', '',
|
|
904
|
-
'class RemoveDuplicateIndexes < ActiveRecord::Migration[' + RAILS_MIGRATION_VERSION + ']', ' def change'];
|
|
905
|
-
data.forEach(function(d) {
|
|
906
|
-
migrationLines.push(' remove_index :' + d.table + ', name: :' + d.duplicate_index);
|
|
907
|
-
});
|
|
908
|
-
migrationLines.push(' end', 'end');
|
|
909
|
-
el('dup-migration-code').textContent = migrationLines.join('\n');
|
|
910
|
-
show(el('dup-migration'));
|
|
911
|
-
}, function() {
|
|
912
|
-
hide(el('dup-loading'));
|
|
913
|
-
el('dup-empty').textContent = 'Failed to scan indexes.';
|
|
914
|
-
show(el('dup-empty'));
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
el('dup-refresh').addEventListener('click', loadDuplicateIndexes);
|
|
919
|
-
el('dup-copy-migration').addEventListener('click', function() {
|
|
920
|
-
copyToClipboard(el('dup-migration-code').textContent, el('dup-copy-migration'));
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
// --- Table Sizes ---
|
|
924
|
-
|
|
925
|
-
function formatMb(mb) {
|
|
926
|
-
if (mb >= 1024) return (mb / 1024).toFixed(2) + ' GB';
|
|
927
|
-
if (mb >= 1) return mb.toFixed(2) + ' MB';
|
|
928
|
-
return (mb * 1024).toFixed(0) + ' KB';
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function sizeBar(pct, color) {
|
|
932
|
-
return '<div style="background:#e9ecef;border-radius:3px;height:8px;width:100%;">' +
|
|
933
|
-
'<div style="background:' + color + ';border-radius:3px;height:8px;width:' + Math.max(pct, 1) + '%;transition:width 0.3s;"></div></div>';
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
var PIE_COLORS = [
|
|
937
|
-
'#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f',
|
|
938
|
-
'#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac',
|
|
939
|
-
'#86bcb6', '#8cd17d', '#b6992d', '#499894', '#d37295'
|
|
940
|
-
];
|
|
941
|
-
|
|
942
|
-
function drawPieChart(canvas, slices, legendEl) {
|
|
943
|
-
var ctx = canvas.getContext('2d');
|
|
944
|
-
var w = canvas.width;
|
|
945
|
-
var h = canvas.height;
|
|
946
|
-
var cx = w / 2;
|
|
947
|
-
var cy = h / 2;
|
|
948
|
-
var r = Math.min(cx, cy) - 4;
|
|
949
|
-
var total = slices.reduce(function(s, sl) { return s + sl.value; }, 0);
|
|
950
|
-
if (total === 0) return;
|
|
951
|
-
|
|
952
|
-
ctx.clearRect(0, 0, w, h);
|
|
953
|
-
var startAngle = -Math.PI / 2;
|
|
954
|
-
|
|
955
|
-
slices.forEach(function(sl, i) {
|
|
956
|
-
var sliceAngle = (sl.value / total) * 2 * Math.PI;
|
|
957
|
-
ctx.beginPath();
|
|
958
|
-
ctx.moveTo(cx, cy);
|
|
959
|
-
ctx.arc(cx, cy, r, startAngle, startAngle + sliceAngle);
|
|
960
|
-
ctx.closePath();
|
|
961
|
-
ctx.fillStyle = PIE_COLORS[i % PIE_COLORS.length];
|
|
962
|
-
ctx.fill();
|
|
963
|
-
ctx.strokeStyle = '#fff';
|
|
964
|
-
ctx.lineWidth = 1.5;
|
|
965
|
-
ctx.stroke();
|
|
966
|
-
startAngle += sliceAngle;
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
// Legend
|
|
970
|
-
legendEl.innerHTML = slices.map(function(sl, i) {
|
|
971
|
-
var pct = ((sl.value / total) * 100).toFixed(1);
|
|
972
|
-
return '<div style="display:flex;align-items:center;gap:6px;">' +
|
|
973
|
-
'<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + PIE_COLORS[i % PIE_COLORS.length] + ';flex-shrink:0;"></span>' +
|
|
974
|
-
'<span>' + escHtml(sl.label) + '</span>' +
|
|
975
|
-
'<span class="mg-text-muted">' + formatMb(sl.value) + ' (' + pct + '%)</span></div>';
|
|
976
|
-
}).join('');
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function loadTableSizes() {
|
|
980
|
-
show(el('sizes-loading'));
|
|
981
|
-
hide(el('sizes-table-wrapper')); hide(el('sizes-chart-wrapper'));
|
|
982
|
-
el('sizes-tbody').innerHTML = '';
|
|
983
|
-
|
|
984
|
-
ajaxGet(ROUTES.table_sizes, {}, function(data) {
|
|
985
|
-
hide(el('sizes-loading'));
|
|
986
|
-
if (!data || !data.length) { el('sizes-total').textContent = '0 tables'; show(el('sizes-table-wrapper')); return; }
|
|
987
|
-
|
|
988
|
-
var totalMb = data.reduce(function(sum, t) { return sum + t.total_mb; }, 0);
|
|
989
|
-
el('sizes-total').textContent = data.length + ' tables, ' + formatMb(totalMb) + ' total';
|
|
990
|
-
var maxMb = data[0].total_mb || 1;
|
|
991
|
-
|
|
992
|
-
// Pie chart: top 10 tables + "Other"
|
|
993
|
-
var topN = data.slice(0, 10);
|
|
994
|
-
var otherMb = data.slice(10).reduce(function(s, t) { return s + t.total_mb; }, 0);
|
|
995
|
-
var slices = topN.map(function(t) { return { label: t.table, value: t.total_mb }; });
|
|
996
|
-
if (otherMb > 0) slices.push({ label: 'Other (' + (data.length - 10) + ' tables)', value: otherMb });
|
|
997
|
-
drawPieChart(el('sizes-pie'), slices, el('sizes-legend'));
|
|
998
|
-
show(el('sizes-chart-wrapper'));
|
|
999
|
-
|
|
1000
|
-
el('sizes-tbody').innerHTML = data.map(function(t) {
|
|
1001
|
-
var pct = (t.total_mb / maxMb) * 100;
|
|
1002
|
-
var color = t.total_mb >= 100 ? '#dc3545' : t.total_mb >= 10 ? '#ffc107' : '#28a745';
|
|
1003
|
-
var rows = t.rows != null ? Number(t.rows).toLocaleString() : '?';
|
|
1004
|
-
var fragText = t.fragmented_mb > 0 ? formatMb(t.fragmented_mb) : '<span class="mg-text-muted">\u2014</span>';
|
|
1005
|
-
if (t.needs_optimize) fragText += ' <span class="mg-badge mg-badge-warning" title="Fragmentation >10% of total size">optimize</span>';
|
|
1006
|
-
var updated = t.updated_at ? new Date(t.updated_at).toLocaleDateString() : '<span class="mg-text-muted">\u2014</span>';
|
|
1007
|
-
var engineBadge = t.engine ? '<span class="mg-badge mg-badge-secondary">' + escHtml(t.engine) + '</span>' : '';
|
|
1008
|
-
return '<tr>' +
|
|
1009
|
-
'<td><strong>' + escHtml(t.table) + '</strong>' +
|
|
1010
|
-
(t.collation ? ' <span class="mg-text-muted" style="font-size:11px">' + escHtml(t.collation) + '</span>' : '') +
|
|
1011
|
-
(t.auto_increment ? ' <span class="mg-text-muted" style="font-size:11px">AI:' + Number(t.auto_increment).toLocaleString() + '</span>' : '') +
|
|
1012
|
-
'</td>' +
|
|
1013
|
-
'<td class="mg-num">' + rows + '</td>' +
|
|
1014
|
-
'<td>' + engineBadge + '</td>' +
|
|
1015
|
-
'<td class="mg-num">' + formatMb(t.data_mb) + '</td>' +
|
|
1016
|
-
'<td class="mg-num">' + formatMb(t.index_mb) + '</td>' +
|
|
1017
|
-
'<td class="mg-num"><strong>' + formatMb(t.total_mb) + '</strong></td>' +
|
|
1018
|
-
'<td class="mg-num">' + fragText + '</td>' +
|
|
1019
|
-
'<td>' + updated + '</td>' +
|
|
1020
|
-
'<td>' + sizeBar(pct, color) + '</td>' +
|
|
1021
|
-
'</tr>';
|
|
1022
|
-
}).join('');
|
|
1023
|
-
show(el('sizes-table-wrapper'));
|
|
1024
|
-
makeSortable(qs('#sizes-table-wrapper .mg-table'));
|
|
1025
|
-
}, function() {
|
|
1026
|
-
hide(el('sizes-loading'));
|
|
1027
|
-
el('sizes-total').textContent = 'Failed to load';
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
el('sizes-refresh').addEventListener('click', loadTableSizes);
|
|
1032
|
-
|
|
1033
|
-
// --- Query Stats ---
|
|
1034
|
-
|
|
1035
|
-
function loadQueryStats() {
|
|
1036
|
-
show(el('qstats-loading'));
|
|
1037
|
-
hide(el('qstats-empty')); hide(el('qstats-table-wrapper')); hide(el('qstats-error'));
|
|
1038
|
-
el('qstats-tbody').innerHTML = '';
|
|
1039
|
-
|
|
1040
|
-
ajaxGet(ROUTES.query_stats, { sort: 'total_time' }, function(data) {
|
|
1041
|
-
hide(el('qstats-loading'));
|
|
1042
|
-
if (data.error) {
|
|
1043
|
-
el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
|
|
1044
|
-
show(el('qstats-error')); return;
|
|
1045
|
-
}
|
|
1046
|
-
if (!data.length) { show(el('qstats-empty')); el('qstats-count').textContent = '0'; return; }
|
|
1047
|
-
el('qstats-count').textContent = data.length + ' queries';
|
|
1048
|
-
el('qstats-tbody').innerHTML = data.map(function(q) {
|
|
1049
|
-
var ratioClass = q.rows_ratio > 100 ? 'mg-badge-danger' : q.rows_ratio > 10 ? 'mg-badge-warning' : '';
|
|
1050
|
-
return '<tr>' +
|
|
1051
|
-
'<td>' + sqlBlock(q.sql, 120) + '</td>' +
|
|
1052
|
-
'<td class="mg-num">' + Number(q.calls).toLocaleString() + '</td>' +
|
|
1053
|
-
'<td class="mg-num">' + formatDurationStyled(q.total_time_ms) + '</td>' +
|
|
1054
|
-
'<td class="mg-num">' + formatDurationStyled(q.avg_time_ms) + '</td>' +
|
|
1055
|
-
'<td class="mg-num">' + formatDurationStyled(q.max_time_ms) + '</td>' +
|
|
1056
|
-
'<td class="mg-num">' + Number(q.rows_examined).toLocaleString() + '</td>' +
|
|
1057
|
-
'<td class="mg-num">' + Number(q.rows_sent).toLocaleString() + '</td>' +
|
|
1058
|
-
'<td class="mg-num">' + (ratioClass ? '<span class="mg-badge ' + ratioClass + '">' + q.rows_ratio + 'x</span>' : q.rows_ratio + 'x') + '</td>' +
|
|
1059
|
-
'</tr>';
|
|
1060
|
-
}).join('');
|
|
1061
|
-
show(el('qstats-table-wrapper'));
|
|
1062
|
-
makeSortable(qs('#qstats-table-wrapper .mg-table'));
|
|
1063
|
-
}, function(json) {
|
|
1064
|
-
hide(el('qstats-loading'));
|
|
1065
|
-
el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load query stats.') + '</div>';
|
|
1066
|
-
show(el('qstats-error'));
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function formatDuration(ms) {
|
|
1071
|
-
if (ms >= 60000) return (ms / 60000).toFixed(1) + ' min';
|
|
1072
|
-
if (ms >= 1000) return (ms / 1000).toFixed(1) + ' s';
|
|
1073
|
-
return ms.toFixed(1) + ' ms';
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// --- Unused Indexes ---
|
|
1077
|
-
|
|
1078
|
-
function loadUnusedIndexes() {
|
|
1079
|
-
show(el('unused-loading'));
|
|
1080
|
-
hide(el('unused-empty')); hide(el('unused-table-wrapper')); hide(el('unused-error'));
|
|
1081
|
-
el('unused-tbody').innerHTML = '';
|
|
1082
|
-
|
|
1083
|
-
ajaxGet(ROUTES.unused_indexes, {}, function(data) {
|
|
1084
|
-
hide(el('unused-loading'));
|
|
1085
|
-
if (data.error) {
|
|
1086
|
-
el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
|
|
1087
|
-
show(el('unused-error')); return;
|
|
1088
|
-
}
|
|
1089
|
-
if (!data.length) { show(el('unused-empty')); el('unused-count').textContent = '0'; return; }
|
|
1090
|
-
el('unused-count').textContent = data.length + ' found';
|
|
1091
|
-
el('unused-tbody').innerHTML = data.map(function(d) {
|
|
1092
|
-
return '<tr>' +
|
|
1093
|
-
'<td><strong>' + escHtml(d.table) + '</strong></td>' +
|
|
1094
|
-
'<td><code>' + escHtml(d.index_name) + '</code></td>' +
|
|
1095
|
-
'<td class="mg-num">' + d.reads + '</td>' +
|
|
1096
|
-
'<td class="mg-num">' + Number(d.writes).toLocaleString() + '</td>' +
|
|
1097
|
-
'<td class="mg-num">' + Number(d.table_rows).toLocaleString() + '</td>' +
|
|
1098
|
-
'<td>' + sqlBlock(d.drop_sql, 0) + '</td>' +
|
|
1099
|
-
'</tr>';
|
|
1100
|
-
}).join('');
|
|
1101
|
-
show(el('unused-table-wrapper'));
|
|
1102
|
-
makeSortable(qs('#unused-table-wrapper .mg-table'));
|
|
1103
|
-
|
|
1104
|
-
// Generate migration
|
|
1105
|
-
var ts = migrationTimestamp();
|
|
1106
|
-
var migrationLines = ['# ' + ts + '_remove_unused_indexes.rb', '',
|
|
1107
|
-
'class RemoveUnusedIndexes < ActiveRecord::Migration[' + RAILS_MIGRATION_VERSION + ']', ' def change'];
|
|
1108
|
-
data.forEach(function(d) {
|
|
1109
|
-
migrationLines.push(' remove_index :' + d.table + ', name: :' + d.index_name);
|
|
1110
|
-
});
|
|
1111
|
-
migrationLines.push(' end', 'end');
|
|
1112
|
-
el('unused-migration-code').textContent = migrationLines.join('\n');
|
|
1113
|
-
show(el('unused-migration'));
|
|
1114
|
-
}, function(json) {
|
|
1115
|
-
hide(el('unused-loading'));
|
|
1116
|
-
el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load unused indexes.') + '</div>';
|
|
1117
|
-
show(el('unused-error'));
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
el('unused-refresh').addEventListener('click', loadUnusedIndexes);
|
|
1122
|
-
el('unused-copy-migration').addEventListener('click', function() {
|
|
1123
|
-
copyToClipboard(el('unused-migration-code').textContent, el('unused-copy-migration'));
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
// --- Server Overview ---
|
|
1127
|
-
|
|
1128
|
-
function usageBar(pct, label) {
|
|
1129
|
-
var color = pct >= 90 ? '#dc3545' : pct >= 70 ? '#ffc107' : '#28a745';
|
|
1130
|
-
return '<div class="mg-usage-bar">' +
|
|
1131
|
-
'<div class="mg-usage-bar-fill" style="width:' + Math.min(pct, 100) + '%;background:' + color + ';"></div>' +
|
|
1132
|
-
'<div class="mg-usage-bar-text">' + label + '</div></div>';
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
function statRow(label, value) {
|
|
1136
|
-
return '<div class="mg-stat-label">' + label + '</div><div class="mg-stat-value">' + value + '</div>';
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function loadServerOverview() {
|
|
1140
|
-
show(el('server-loading'));
|
|
1141
|
-
hide(el('server-content')); hide(el('server-error'));
|
|
1142
|
-
|
|
1143
|
-
ajaxGet(ROUTES.server_overview, {}, function(data) {
|
|
1144
|
-
hide(el('server-loading'));
|
|
1145
|
-
if (data.error) {
|
|
1146
|
-
el('server-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
|
|
1147
|
-
show(el('server-error')); return;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
var s = data.server;
|
|
1151
|
-
var c = data.connections;
|
|
1152
|
-
var db = data.innodb;
|
|
1153
|
-
var q = data.queries;
|
|
1154
|
-
|
|
1155
|
-
// Server info
|
|
1156
|
-
el('server-info').innerHTML =
|
|
1157
|
-
statRow('Version', '<code>' + escHtml(s.version) + '</code>') +
|
|
1158
|
-
statRow('Uptime', escHtml(s.uptime)) +
|
|
1159
|
-
statRow('Queries/sec', q.qps) +
|
|
1160
|
-
statRow('Total Queries', Number(q.questions).toLocaleString()) +
|
|
1161
|
-
statRow('Slow Queries', Number(q.slow_queries).toLocaleString());
|
|
1162
|
-
|
|
1163
|
-
// Connections
|
|
1164
|
-
el('conn-bar').innerHTML = usageBar(c.usage_pct, c.current + ' / ' + c.max + ' (' + c.usage_pct + '%)');
|
|
1165
|
-
el('conn-info').innerHTML =
|
|
1166
|
-
statRow('Threads Running', c.threads_running) +
|
|
1167
|
-
statRow('Threads Cached', c.threads_cached) +
|
|
1168
|
-
statRow('Threads Created', Number(c.threads_created).toLocaleString()) +
|
|
1169
|
-
statRow('Max Used', c.max_used) +
|
|
1170
|
-
statRow('Aborted Connects', Number(c.aborted_connects).toLocaleString()) +
|
|
1171
|
-
statRow('Aborted Clients', Number(c.aborted_clients).toLocaleString());
|
|
1172
|
-
|
|
1173
|
-
// InnoDB
|
|
1174
|
-
var poolUsedPct = db.buffer_pool_pages_total > 0
|
|
1175
|
-
? (((db.buffer_pool_pages_total - db.buffer_pool_pages_free) / db.buffer_pool_pages_total) * 100).toFixed(1)
|
|
1176
|
-
: 0;
|
|
1177
|
-
el('innodb-bar').innerHTML = usageBar(parseFloat(poolUsedPct), db.buffer_pool_mb + ' MB (' + poolUsedPct + '% used)');
|
|
1178
|
-
el('innodb-info').innerHTML =
|
|
1179
|
-
statRow('Hit Rate', db.buffer_pool_hit_rate + '%') +
|
|
1180
|
-
statRow('Dirty Pages', Number(db.buffer_pool_pages_dirty).toLocaleString()) +
|
|
1181
|
-
statRow('Free Pages', Number(db.buffer_pool_pages_free).toLocaleString()) +
|
|
1182
|
-
statRow('Row Lock Waits', Number(db.row_lock_waits).toLocaleString()) +
|
|
1183
|
-
statRow('Row Lock Time', formatDuration(db.row_lock_time_ms));
|
|
1184
|
-
|
|
1185
|
-
// Query activity
|
|
1186
|
-
var tmpBadge = q.tmp_disk_pct > 25
|
|
1187
|
-
? '<span class="mg-badge mg-badge-danger">' + q.tmp_disk_pct + '%</span>'
|
|
1188
|
-
: q.tmp_disk_pct + '%';
|
|
1189
|
-
el('query-info').innerHTML =
|
|
1190
|
-
statRow('Tmp Tables (disk)', Number(q.tmp_disk_tables).toLocaleString() + ' / ' + Number(q.tmp_tables).toLocaleString() + ' ' + tmpBadge) +
|
|
1191
|
-
statRow('Full Joins (no index)', Number(q.select_full_join).toLocaleString()) +
|
|
1192
|
-
statRow('Sort Merge Passes', Number(q.sort_merge_passes).toLocaleString());
|
|
1193
|
-
|
|
1194
|
-
show(el('server-content'));
|
|
1195
|
-
}, function(json) {
|
|
1196
|
-
hide(el('server-loading'));
|
|
1197
|
-
el('server-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load server overview.') + '</div>';
|
|
1198
|
-
show(el('server-error'));
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
el('server-refresh').addEventListener('click', loadServerOverview);
|
|
1203
|
-
|
|
1204
|
-
// --- AI Feature Handlers ---
|
|
1205
|
-
|
|
1206
|
-
// Generic AI call helper
|
|
1207
|
-
function aiCall(url, data, onSuccess, onError) {
|
|
1208
|
-
ajax('POST', url, data, function(result) {
|
|
1209
|
-
if (result.error) { onError(result.error); return; }
|
|
1210
|
-
onSuccess(result);
|
|
1211
|
-
}, function(json) {
|
|
1212
|
-
onError((json && json.error) || 'AI request failed.');
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function showAiQueryResult(title, html) {
|
|
1217
|
-
el('ai-query-title').innerHTML = '<strong>⚡ ' + escHtml(title) + '</strong>';
|
|
1218
|
-
el('ai-query-content').innerHTML = html;
|
|
1219
|
-
show(el('ai-query-result'));
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
el('ai-query-close').addEventListener('click', function() { hide(el('ai-query-result')); });
|
|
1223
|
-
|
|
1224
|
-
// Describe Query
|
|
1225
|
-
var sqlDescribe = el('sql-describe');
|
|
1226
|
-
if (sqlDescribe) {
|
|
1227
|
-
sqlDescribe.addEventListener('click', function() {
|
|
1228
|
-
var sql = el('sql-input').value.trim();
|
|
1229
|
-
if (!sql) return;
|
|
1230
|
-
sqlDescribe.disabled = true;
|
|
1231
|
-
sqlDescribe.innerHTML = '<span class="mg-spinner"></span>';
|
|
1232
|
-
hide(el('ai-query-result'));
|
|
1233
|
-
aiCall(ROUTES.describe_query, { sql: sql }, function(data) {
|
|
1234
|
-
showAiQueryResult('Query Description', formatMarkdown(data.explanation || data.raw || 'No explanation returned.'));
|
|
1235
|
-
sqlDescribe.disabled = false;
|
|
1236
|
-
sqlDescribe.innerHTML = '⚡ Describe';
|
|
1237
|
-
}, function(err) {
|
|
1238
|
-
showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
|
|
1239
|
-
sqlDescribe.disabled = false;
|
|
1240
|
-
sqlDescribe.innerHTML = '⚡ Describe';
|
|
1241
|
-
});
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Rewrite Query
|
|
1246
|
-
var sqlRewrite = el('sql-rewrite');
|
|
1247
|
-
if (sqlRewrite) {
|
|
1248
|
-
sqlRewrite.addEventListener('click', function() {
|
|
1249
|
-
var sql = el('sql-input').value.trim();
|
|
1250
|
-
if (!sql) return;
|
|
1251
|
-
sqlRewrite.disabled = true;
|
|
1252
|
-
sqlRewrite.innerHTML = '<span class="mg-spinner"></span>';
|
|
1253
|
-
hide(el('ai-query-result'));
|
|
1254
|
-
aiCall(ROUTES.rewrite_query, { sql: sql }, function(data) {
|
|
1255
|
-
var html = '';
|
|
1256
|
-
if (data.rewritten) {
|
|
1257
|
-
html += '<strong>Rewritten Query:</strong><pre class="mg-pre"><code>' + escHtml(data.rewritten) + '</code></pre>';
|
|
1258
|
-
}
|
|
1259
|
-
if (data.changes) {
|
|
1260
|
-
html += '<strong>Changes:</strong><br>' + formatMarkdown(data.changes);
|
|
1261
|
-
}
|
|
1262
|
-
if (!data.rewritten && !data.changes) {
|
|
1263
|
-
html = formatMarkdown(data.raw || 'No rewrite suggestions.');
|
|
1264
|
-
}
|
|
1265
|
-
showAiQueryResult('Query Rewrite', html);
|
|
1266
|
-
sqlRewrite.disabled = false;
|
|
1267
|
-
sqlRewrite.innerHTML = '⚡ Rewrite';
|
|
1268
|
-
}, function(err) {
|
|
1269
|
-
showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
|
|
1270
|
-
sqlRewrite.disabled = false;
|
|
1271
|
-
sqlRewrite.innerHTML = '⚡ Rewrite';
|
|
1272
|
-
});
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// Index Advisor (on EXPLAIN results)
|
|
1277
|
-
var indexAdvisor = el('explain-index-advisor');
|
|
1278
|
-
if (indexAdvisor) {
|
|
1279
|
-
indexAdvisor.addEventListener('click', function() {
|
|
1280
|
-
indexAdvisor.disabled = true;
|
|
1281
|
-
indexAdvisor.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
1282
|
-
hide(el('optimize-results'));
|
|
1283
|
-
|
|
1284
|
-
var data = { sql: lastExplainSql };
|
|
1285
|
-
lastExplainRows.forEach(function(row, i) {
|
|
1286
|
-
row.forEach(function(val, j) {
|
|
1287
|
-
data['explain_rows[' + i + '][' + j + ']'] = val == null ? '' : val;
|
|
1288
|
-
});
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
aiCall(ROUTES.index_advisor, data, function(resp) {
|
|
1292
|
-
el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.');
|
|
1293
|
-
show(el('optimize-results'));
|
|
1294
|
-
indexAdvisor.disabled = false;
|
|
1295
|
-
indexAdvisor.innerHTML = '⚡ Index Advisor';
|
|
1296
|
-
}, function(err) {
|
|
1297
|
-
el('optimize-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
1298
|
-
show(el('optimize-results'));
|
|
1299
|
-
indexAdvisor.disabled = false;
|
|
1300
|
-
indexAdvisor.innerHTML = '⚡ Index Advisor';
|
|
1301
|
-
});
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
// Server: Root Cause Analysis
|
|
1306
|
-
var rootCauseBtn = el('server-root-cause');
|
|
1307
|
-
if (rootCauseBtn) {
|
|
1308
|
-
rootCauseBtn.addEventListener('click', function() {
|
|
1309
|
-
rootCauseBtn.disabled = true;
|
|
1310
|
-
rootCauseBtn.innerHTML = '<span class="mg-spinner"></span> Diagnosing...';
|
|
1311
|
-
hide(el('server-ai-result'));
|
|
1312
|
-
aiCall(ROUTES.root_cause, {}, function(data) {
|
|
1313
|
-
el('server-ai-title').innerHTML = '<strong>⚡ Root Cause Analysis</strong>';
|
|
1314
|
-
el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.');
|
|
1315
|
-
show(el('server-ai-result'));
|
|
1316
|
-
rootCauseBtn.disabled = false;
|
|
1317
|
-
rootCauseBtn.innerHTML = '⚡ Why is it slow?';
|
|
1318
|
-
}, function(err) {
|
|
1319
|
-
el('server-ai-title').innerHTML = '<strong>⚡ Error</strong>';
|
|
1320
|
-
el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
1321
|
-
show(el('server-ai-result'));
|
|
1322
|
-
rootCauseBtn.disabled = false;
|
|
1323
|
-
rootCauseBtn.innerHTML = '⚡ Why is it slow?';
|
|
1324
|
-
});
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
el('server-ai-close').addEventListener('click', function() { hide(el('server-ai-result')); });
|
|
1329
|
-
|
|
1330
|
-
// Server: Anomaly Detection
|
|
1331
|
-
var anomalyBtn = el('server-anomaly');
|
|
1332
|
-
if (anomalyBtn) {
|
|
1333
|
-
anomalyBtn.addEventListener('click', function() {
|
|
1334
|
-
anomalyBtn.disabled = true;
|
|
1335
|
-
anomalyBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
1336
|
-
hide(el('server-ai-result'));
|
|
1337
|
-
aiCall(ROUTES.anomaly_detection, {}, function(data) {
|
|
1338
|
-
el('server-ai-title').innerHTML = '<strong>⚡ Query Health Report</strong>';
|
|
1339
|
-
el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.');
|
|
1340
|
-
show(el('server-ai-result'));
|
|
1341
|
-
anomalyBtn.disabled = false;
|
|
1342
|
-
anomalyBtn.innerHTML = '⚡ Anomaly Detection';
|
|
1343
|
-
}, function(err) {
|
|
1344
|
-
el('server-ai-title').innerHTML = '<strong>⚡ Error</strong>';
|
|
1345
|
-
el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
1346
|
-
show(el('server-ai-result'));
|
|
1347
|
-
anomalyBtn.disabled = false;
|
|
1348
|
-
anomalyBtn.innerHTML = '⚡ Anomaly Detection';
|
|
1349
|
-
});
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// Schema Review
|
|
1354
|
-
var schemaBtn = el('schema-review-btn');
|
|
1355
|
-
if (schemaBtn) {
|
|
1356
|
-
schemaBtn.addEventListener('click', function() {
|
|
1357
|
-
schemaBtn.disabled = true;
|
|
1358
|
-
schemaBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
1359
|
-
hide(el('schema-result'));
|
|
1360
|
-
var table = el('schema-table').value;
|
|
1361
|
-
aiCall(ROUTES.schema_review, { table: table }, function(data) {
|
|
1362
|
-
el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '');
|
|
1363
|
-
show(el('schema-result'));
|
|
1364
|
-
schemaBtn.disabled = false;
|
|
1365
|
-
schemaBtn.innerHTML = '⚡ Analyze Schema';
|
|
1366
|
-
}, function(err) {
|
|
1367
|
-
el('schema-result-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
1368
|
-
show(el('schema-result'));
|
|
1369
|
-
schemaBtn.disabled = false;
|
|
1370
|
-
schemaBtn.innerHTML = '⚡ Analyze Schema';
|
|
1371
|
-
});
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// Migration Risk
|
|
1376
|
-
var migrationBtn = el('migration-assess-btn');
|
|
1377
|
-
if (migrationBtn) {
|
|
1378
|
-
migrationBtn.addEventListener('click', function() {
|
|
1379
|
-
var migration = el('migration-input').value.trim();
|
|
1380
|
-
if (!migration) {
|
|
1381
|
-
el('migration-risk-badge').innerHTML = '';
|
|
1382
|
-
el('migration-result-content').innerHTML = '<div class="mg-alert mg-alert-warning">Please paste a Rails migration or DDL statement above.</div>';
|
|
1383
|
-
show(el('migration-result'));
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
migrationBtn.disabled = true;
|
|
1387
|
-
migrationBtn.innerHTML = '<span class="mg-spinner"></span> Assessing...';
|
|
1388
|
-
hide(el('migration-result'));
|
|
1389
|
-
aiCall(ROUTES.migration_risk, { migration: migration }, function(data) {
|
|
1390
|
-
var level = (data.risk_level || '').toLowerCase();
|
|
1391
|
-
var badgeClass = level === 'critical' ? 'mg-badge-danger' : level === 'high' ? 'mg-badge-danger' : level === 'medium' ? 'mg-badge-warning' : 'mg-badge-info';
|
|
1392
|
-
el('migration-risk-badge').innerHTML = level ? '<span class="mg-badge ' + badgeClass + '" style="font-size:14px;padding:4px 12px;">Risk: ' + level.toUpperCase() + '</span>' : '';
|
|
1393
|
-
el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '');
|
|
1394
|
-
show(el('migration-result'));
|
|
1395
|
-
migrationBtn.disabled = false;
|
|
1396
|
-
migrationBtn.innerHTML = '⚡ Assess Risk';
|
|
1397
|
-
}, function(err) {
|
|
1398
|
-
el('migration-risk-badge').innerHTML = '';
|
|
1399
|
-
el('migration-result-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
1400
|
-
show(el('migration-result'));
|
|
1401
|
-
migrationBtn.disabled = false;
|
|
1402
|
-
migrationBtn.innerHTML = '⚡ Assess Risk';
|
|
1403
|
-
});
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// --- SQL to Builder sync ---
|
|
1408
|
-
|
|
1409
|
-
function parseSqlToBuilder(sql) {
|
|
1410
|
-
if (!sql) return;
|
|
1411
|
-
if (sql.match(/\b(JOIN|GROUP\s+BY|HAVING|UNION)\b/i) || (sql.match(/SELECT/gi) || []).length > 1) return;
|
|
1412
|
-
var tableMatch = sql.match(/FROM\s+`?(\w+)`?/i);
|
|
1413
|
-
if (!tableMatch) return;
|
|
1414
|
-
var tableName = tableMatch[1];
|
|
1415
|
-
if (!qs('#vb-table option[value="' + tableName + '"]')) return;
|
|
1416
|
-
|
|
1417
|
-
var colMatch = sql.match(/SELECT\s+(.*?)\s+FROM/i);
|
|
1418
|
-
var selectedCols = [];
|
|
1419
|
-
if (colMatch && colMatch[1].trim() !== '*') {
|
|
1420
|
-
colMatch[1].split(',').forEach(function(p) { var n = p.trim().replace(/`/g, ''); if (n) selectedCols.push(n); });
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
var whereMatch = sql.match(/WHERE\s+(.*?)(?:\s+ORDER\s+BY\b|\s+LIMIT\b|\s*$)/i);
|
|
1424
|
-
var conditions = whereMatch ? parseWhereClause(whereMatch[1]) : [];
|
|
1425
|
-
|
|
1426
|
-
var orderMatch = sql.match(/ORDER\s+BY\s+(.*?)(?:\s+LIMIT\b|\s*$)/i);
|
|
1427
|
-
var orders = [];
|
|
1428
|
-
if (orderMatch) {
|
|
1429
|
-
orderMatch[1].split(',').forEach(function(p) {
|
|
1430
|
-
var m = p.trim().match(/`?(\w+)`?\s*(ASC|DESC)?/i);
|
|
1431
|
-
if (m) orders.push({ column: m[1], direction: (m[2] || 'ASC').toUpperCase() });
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
el('vb-table').value = tableName;
|
|
1436
|
-
loadColumnsForTable(tableName, function() {
|
|
1437
|
-
if (selectedCols.length) {
|
|
1438
|
-
qsa('.vb-col-check').forEach(function(c) { c.checked = selectedCols.indexOf(c.value) !== -1; });
|
|
1439
|
-
}
|
|
1440
|
-
el('vb-filters').innerHTML = '';
|
|
1441
|
-
conditions.forEach(function(cond) {
|
|
1442
|
-
addFilterRow(true);
|
|
1443
|
-
var row = el('vb-filters').lastElementChild;
|
|
1444
|
-
qs('.vb-filter-col', row).value = cond.column;
|
|
1445
|
-
var colType = columnTypeMap[cond.column] || 'string';
|
|
1446
|
-
var opSel = qs('.vb-filter-op', row);
|
|
1447
|
-
opSel.innerHTML = operatorsForType(colType).map(function(o) { return '<option>' + o + '</option>'; }).join('');
|
|
1448
|
-
opSel.value = cond.operator;
|
|
1449
|
-
updateValueInput(row, colType, cond.operator);
|
|
1450
|
-
if (cond.operator !== 'IS NULL' && cond.operator !== 'IS NOT NULL') {
|
|
1451
|
-
var valEl = qs('.vb-filter-val', row);
|
|
1452
|
-
if (valEl) valEl.value = cond.value;
|
|
1453
|
-
if (cond.operator === 'BETWEEN') { var endEl = qs('.vb-filter-val-end', row); if (endEl) endEl.value = cond.endValue; }
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
el('vb-orders').innerHTML = '';
|
|
1457
|
-
orders.forEach(function(ord) {
|
|
1458
|
-
addOrderRow(true);
|
|
1459
|
-
var row = el('vb-orders').lastElementChild;
|
|
1460
|
-
qs('.vb-order-col', row).value = ord.column;
|
|
1461
|
-
qs('.vb-order-dir', row).value = ord.direction;
|
|
1462
|
-
});
|
|
1463
|
-
updateGeneratedSql();
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
function parseWhereClause(str) {
|
|
1468
|
-
var conditions = [];
|
|
1469
|
-
var betweenRe = /`?(\w+)`?\s+BETWEEN\s+'([^']*)'\s+AND\s+'([^']*)'/gi;
|
|
1470
|
-
var m;
|
|
1471
|
-
while ((m = betweenRe.exec(str)) !== null) conditions.push({ column: m[1], operator: 'BETWEEN', value: m[2], endValue: m[3] });
|
|
1472
|
-
var remaining = str.replace(betweenRe, '{{B}}');
|
|
1473
|
-
remaining.split(/\s+AND\s+/i).forEach(function(part) {
|
|
1474
|
-
part = part.trim();
|
|
1475
|
-
if (!part || part === '{{B}}') return;
|
|
1476
|
-
var m;
|
|
1477
|
-
if ((m = part.match(/`?(\w+)`?\s+IS\s+NOT\s+NULL/i))) { conditions.push({ column: m[1], operator: 'IS NOT NULL', value: '' }); return; }
|
|
1478
|
-
if ((m = part.match(/`?(\w+)`?\s+IS\s+NULL/i))) { conditions.push({ column: m[1], operator: 'IS NULL', value: '' }); return; }
|
|
1479
|
-
if ((m = part.match(/`?(\w+)`?\s+LIKE\s+'([^']*)'/i))) { conditions.push({ column: m[1], operator: 'LIKE', value: m[2] }); return; }
|
|
1480
|
-
if ((m = part.match(/`?(\w+)`?\s*(!=|>=|<=|=|>|<)\s*'([^']*)'/))) { conditions.push({ column: m[1], operator: m[2], value: m[3] }); return; }
|
|
1481
|
-
if ((m = part.match(/`?(\w+)`?\s*(!=|>=|<=|=|>|<)\s*(\d+\.?\d*)/))) { conditions.push({ column: m[1], operator: m[2], value: m[3] }); return; }
|
|
1482
|
-
});
|
|
1483
|
-
return conditions;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
// --- Utilities ---
|
|
1487
|
-
|
|
1488
|
-
function setBtnLoading(ids, loading) {
|
|
1489
|
-
ids.forEach(function(id) {
|
|
1490
|
-
var btn = el(id);
|
|
1491
|
-
if (!btn) return;
|
|
1492
|
-
btn.disabled = loading;
|
|
1493
|
-
if (loading) { btn.dataset.origHtml = btn.innerHTML; btn.innerHTML = '<span class="mg-spinner"></span>'; }
|
|
1494
|
-
else if (btn.dataset.origHtml) { btn.innerHTML = btn.dataset.origHtml; }
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
function formatMarkdown(text) {
|
|
1499
|
-
if (!text) return '';
|
|
1500
|
-
// Normalize literal \n from JSON strings to actual newlines
|
|
1501
|
-
text = text.replace(/\\n/g, '\n');
|
|
1502
|
-
return text
|
|
1503
|
-
.replace(/```sql\n?([\s\S]*?)```/g, '<pre class="mg-pre"><code>$1</code></pre>')
|
|
1504
|
-
.replace(/```\n?([\s\S]*?)```/g, '<pre class="mg-pre"><code>$1</code></pre>')
|
|
1505
|
-
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
1506
|
-
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
1507
|
-
.replace(/^####\s+(.+)$/gm, '<h5 style="margin:12px 0 4px;">$1</h5>')
|
|
1508
|
-
.replace(/^###\s+(.+)$/gm, '<h4 style="margin:16px 0 6px;">$1</h4>')
|
|
1509
|
-
.replace(/^##\s+(.+)$/gm, '<h3 style="margin:20px 0 8px;">$1</h3>')
|
|
1510
|
-
.replace(/^#\s+(.+)$/gm, '<h3 style="margin:20px 0 8px;">$1</h3>')
|
|
1511
|
-
.replace(/^---+$/gm, '<hr style="margin:12px 0;">')
|
|
1512
|
-
.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>')
|
|
1513
|
-
.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>')
|
|
1514
|
-
.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul style="margin:4px 0 4px 16px;padding:0;">$1</ul>')
|
|
1515
|
-
.replace(/<\/ul>\s*<ul[^>]*>/g, '')
|
|
1516
|
-
.replace(/\n\n/g, '<br><br>')
|
|
1517
|
-
.replace(/\n/g, '<br>');
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
function formatFindings(text) {
|
|
1521
|
-
if (!text) return '<div class="mg-text-muted">No findings.</div>';
|
|
1522
|
-
text = text.replace(/\\n/g, '\n');
|
|
1523
|
-
|
|
1524
|
-
// Split into severity sections
|
|
1525
|
-
var sections = [];
|
|
1526
|
-
var current = null;
|
|
1527
|
-
text.split('\n').forEach(function(line) {
|
|
1528
|
-
var heading = line.match(/^#{1,3}\s+(.+)/);
|
|
1529
|
-
if (heading) {
|
|
1530
|
-
var title = heading[1].replace(/\*\*/g, '');
|
|
1531
|
-
var severity = 'info';
|
|
1532
|
-
var titleLower = title.toLowerCase();
|
|
1533
|
-
if (titleLower.indexOf('critical') !== -1) severity = 'danger';
|
|
1534
|
-
else if (titleLower.indexOf('warning') !== -1) severity = 'warning';
|
|
1535
|
-
else if (titleLower.indexOf('suggestion') !== -1 || titleLower.indexOf('info') !== -1) severity = 'info';
|
|
1536
|
-
current = { title: title, severity: severity, lines: [] };
|
|
1537
|
-
sections.push(current);
|
|
1538
|
-
} else if (current) {
|
|
1539
|
-
current.lines.push(line);
|
|
1540
|
-
} else {
|
|
1541
|
-
if (!sections.length) sections.push({ title: '', severity: 'info', lines: [] });
|
|
1542
|
-
if (!current) current = sections[0];
|
|
1543
|
-
current.lines.push(line);
|
|
1544
|
-
}
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
if (!sections.length) return formatMarkdown(text);
|
|
1548
|
-
|
|
1549
|
-
var badgeColors = { danger: '#dc3545', warning: '#ffc107', info: '#17a2b8' };
|
|
1550
|
-
var bgColors = { danger: '#fff5f5', warning: '#fffbeb', info: '#f0f9ff' };
|
|
1551
|
-
var borderColors = { danger: '#f5c6cb', warning: '#ffeeba', info: '#bee5eb' };
|
|
1552
|
-
|
|
1553
|
-
return sections.map(function(sec) {
|
|
1554
|
-
var content = formatMarkdown(sec.lines.join('\n').trim());
|
|
1555
|
-
if (!content || content === '<br>') return '';
|
|
1556
|
-
var badge = badgeColors[sec.severity] || badgeColors.info;
|
|
1557
|
-
var bg = bgColors[sec.severity] || bgColors.info;
|
|
1558
|
-
var border = borderColors[sec.severity] || borderColors.info;
|
|
1559
|
-
return '<div class="mg-card mg-mb" style="border-left:4px solid ' + badge + ';background:' + bg + ';border-color:' + border + ';">' +
|
|
1560
|
-
(sec.title ? '<div class="mg-card-header" style="background:transparent;border-bottom:1px solid ' + border + ';"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
|
|
1561
|
-
'<div class="mg-card-body" style="font-size:13px;">' + content + '</div></div>';
|
|
1562
|
-
}).filter(function(s) { return s; }).join('');
|
|
1563
|
-
}
|
|
1564
|
-
})();
|
|
1565
|
-
</script>
|