mysql_genius 0.1.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 +7 -0
- data/.github/workflows/ci.yml +53 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +295 -0
- data/Rakefile +6 -0
- data/app/controllers/concerns/mysql_genius/ai_features.rb +360 -0
- data/app/controllers/concerns/mysql_genius/database_analysis.rb +259 -0
- data/app/controllers/concerns/mysql_genius/query_execution.rb +129 -0
- data/app/controllers/mysql_genius/base_controller.rb +18 -0
- data/app/controllers/mysql_genius/queries_controller.rb +54 -0
- data/app/services/mysql_genius/ai_client.rb +84 -0
- data/app/services/mysql_genius/ai_optimization_service.rb +56 -0
- data/app/services/mysql_genius/ai_suggestion_service.rb +56 -0
- data/app/views/layouts/mysql_genius/application.html.erb +116 -0
- data/app/views/mysql_genius/queries/_shared_results.html.erb +56 -0
- data/app/views/mysql_genius/queries/_tab_ai_tools.html.erb +43 -0
- data/app/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +24 -0
- data/app/views/mysql_genius/queries/_tab_query_stats.html.erb +36 -0
- data/app/views/mysql_genius/queries/_tab_server.html.erb +54 -0
- data/app/views/mysql_genius/queries/_tab_slow_queries.html.erb +17 -0
- data/app/views/mysql_genius/queries/_tab_sql_query.html.erb +40 -0
- data/app/views/mysql_genius/queries/_tab_table_sizes.html.erb +31 -0
- data/app/views/mysql_genius/queries/_tab_unused_indexes.html.erb +25 -0
- data/app/views/mysql_genius/queries/_tab_visual_builder.html.erb +61 -0
- data/app/views/mysql_genius/queries/index.html.erb +1185 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/routes.rb +24 -0
- data/docs/screenshots/ai_tools.png +0 -0
- data/docs/screenshots/duplicate_indexes.png +0 -0
- data/docs/screenshots/query_stats.png +0 -0
- data/docs/screenshots/server.png +0 -0
- data/docs/screenshots/sql_query.png +0 -0
- data/docs/screenshots/table_sizes.png +0 -0
- data/docs/screenshots/visual_builder.png +0 -0
- data/lib/mysql_genius/configuration.rb +96 -0
- data/lib/mysql_genius/engine.rb +12 -0
- data/lib/mysql_genius/slow_query_monitor.rb +38 -0
- data/lib/mysql_genius/sql_validator.rb +55 -0
- data/lib/mysql_genius/version.rb +3 -0
- data/lib/mysql_genius.rb +23 -0
- data/mysql_genius.gemspec +34 -0
- metadata +122 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
<h4>🐘 MySQLGenius</h4>
|
|
2
|
+
|
|
3
|
+
<div class="mg-tabs">
|
|
4
|
+
<button class="mg-tab active" data-tab="visual">Visual Builder</button>
|
|
5
|
+
<button class="mg-tab" data-tab="sql">SQL Query</button>
|
|
6
|
+
<button class="mg-tab" data-tab="slow">Slow Queries</button>
|
|
7
|
+
<button class="mg-tab" data-tab="indexes">Duplicate Indexes</button>
|
|
8
|
+
<button class="mg-tab" data-tab="sizes">Table Sizes</button>
|
|
9
|
+
<button class="mg-tab" data-tab="qstats">Query Stats</button>
|
|
10
|
+
<button class="mg-tab" data-tab="unused">Unused Indexes</button>
|
|
11
|
+
<button class="mg-tab" data-tab="server">Server</button>
|
|
12
|
+
<% if @ai_enabled %>
|
|
13
|
+
<button class="mg-tab" data-tab="aitools">AI Tools</button>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<%= render "mysql_genius/queries/tab_visual_builder" %>
|
|
18
|
+
<%= render "mysql_genius/queries/tab_sql_query" %>
|
|
19
|
+
<%= render "mysql_genius/queries/tab_slow_queries" %>
|
|
20
|
+
<%= render "mysql_genius/queries/tab_duplicate_indexes" %>
|
|
21
|
+
<%= render "mysql_genius/queries/tab_table_sizes" %>
|
|
22
|
+
<%= render "mysql_genius/queries/tab_query_stats" %>
|
|
23
|
+
<%= render "mysql_genius/queries/tab_unused_indexes" %>
|
|
24
|
+
<%= render "mysql_genius/queries/tab_server" %>
|
|
25
|
+
<% if @ai_enabled %>
|
|
26
|
+
<%= render "mysql_genius/queries/tab_ai_tools" %>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<%= render "mysql_genius/queries/shared_results" %>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
(function() {
|
|
33
|
+
"use strict";
|
|
34
|
+
|
|
35
|
+
var ROUTES = {
|
|
36
|
+
columns: '<%= mysql_genius.columns_path %>',
|
|
37
|
+
execute: '<%= mysql_genius.execute_path %>',
|
|
38
|
+
explain: '<%= mysql_genius.explain_path %>',
|
|
39
|
+
suggest: '<%= mysql_genius.suggest_path %>',
|
|
40
|
+
optimize: '<%= mysql_genius.optimize_path %>',
|
|
41
|
+
slow_queries: '<%= mysql_genius.slow_queries_path %>',
|
|
42
|
+
duplicate_indexes: '<%= mysql_genius.duplicate_indexes_path %>',
|
|
43
|
+
table_sizes: '<%= mysql_genius.table_sizes_path %>',
|
|
44
|
+
query_stats: '<%= mysql_genius.query_stats_path %>',
|
|
45
|
+
unused_indexes: '<%= mysql_genius.unused_indexes_path %>',
|
|
46
|
+
server_overview: '<%= mysql_genius.server_overview_path %>',
|
|
47
|
+
describe_query: '<%= mysql_genius.describe_query_path %>',
|
|
48
|
+
schema_review: '<%= mysql_genius.schema_review_path %>',
|
|
49
|
+
rewrite_query: '<%= mysql_genius.rewrite_query_path %>',
|
|
50
|
+
index_advisor: '<%= mysql_genius.index_advisor_path %>',
|
|
51
|
+
anomaly_detection: '<%= mysql_genius.anomaly_detection_path %>',
|
|
52
|
+
root_cause: '<%= mysql_genius.root_cause_path %>',
|
|
53
|
+
migration_risk: '<%= mysql_genius.migration_risk_path %>'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
|
57
|
+
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
|
58
|
+
var currentColumns = [];
|
|
59
|
+
var columnTypeMap = {};
|
|
60
|
+
var lastExplainSql = '';
|
|
61
|
+
var lastExplainRows = [];
|
|
62
|
+
|
|
63
|
+
// --- Helpers ---
|
|
64
|
+
|
|
65
|
+
function el(id) { return document.getElementById(id); }
|
|
66
|
+
function qs(sel, parent) { return (parent || document).querySelector(sel); }
|
|
67
|
+
function qsa(sel, parent) { return Array.from((parent || document).querySelectorAll(sel)); }
|
|
68
|
+
function show(e) { e.classList.remove('mg-hidden'); }
|
|
69
|
+
function hide(e) { e.classList.add('mg-hidden'); }
|
|
70
|
+
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
71
|
+
|
|
72
|
+
function ajax(method, url, data, onSuccess, onError) {
|
|
73
|
+
var xhr = new XMLHttpRequest();
|
|
74
|
+
xhr.open(method, url, true);
|
|
75
|
+
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
|
|
76
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
77
|
+
xhr.onload = function() {
|
|
78
|
+
var json;
|
|
79
|
+
try { json = JSON.parse(xhr.responseText); } catch(e) { json = null; }
|
|
80
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
81
|
+
onSuccess(json);
|
|
82
|
+
} else {
|
|
83
|
+
(onError || function(){})(json, xhr.status);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
xhr.onerror = function() { (onError || function(){})(null, 0); };
|
|
87
|
+
if (data && method !== 'GET') {
|
|
88
|
+
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
89
|
+
var parts = [];
|
|
90
|
+
for (var k in data) {
|
|
91
|
+
if (Array.isArray(data[k])) {
|
|
92
|
+
data[k].forEach(function(v, i) {
|
|
93
|
+
if (typeof v === 'object') {
|
|
94
|
+
for (var vk in v) parts.push(encodeURIComponent(k + '[' + i + '][' + vk + ']') + '=' + encodeURIComponent(v[vk]));
|
|
95
|
+
} else {
|
|
96
|
+
parts.push(encodeURIComponent(k + '[]') + '=' + encodeURIComponent(v));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
xhr.send(parts.join('&'));
|
|
104
|
+
} else if (method === 'GET' && data) {
|
|
105
|
+
xhr.send();
|
|
106
|
+
} else {
|
|
107
|
+
xhr.send();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ajaxGet(url, params, onSuccess, onError) {
|
|
112
|
+
var query = Object.keys(params).map(function(k) {
|
|
113
|
+
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
|
|
114
|
+
}).join('&');
|
|
115
|
+
ajax('GET', url + '?' + query, null, onSuccess, onError);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Tabs ---
|
|
119
|
+
|
|
120
|
+
qsa('.mg-tab').forEach(function(tab) {
|
|
121
|
+
tab.addEventListener('click', function() {
|
|
122
|
+
qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
123
|
+
qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
124
|
+
tab.classList.add('active');
|
|
125
|
+
el('tab-' + tab.dataset.tab).classList.add('active');
|
|
126
|
+
if (tab.dataset.tab === 'slow') loadSlowQueries();
|
|
127
|
+
if (tab.dataset.tab === 'indexes') loadDuplicateIndexes();
|
|
128
|
+
if (tab.dataset.tab === 'sizes') loadTableSizes();
|
|
129
|
+
if (tab.dataset.tab === 'qstats') loadQueryStats();
|
|
130
|
+
if (tab.dataset.tab === 'unused') loadUnusedIndexes();
|
|
131
|
+
if (tab.dataset.tab === 'server') loadServerOverview();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// --- Visual Builder ---
|
|
136
|
+
|
|
137
|
+
var typeLabels = { string: 'text', text: 'text', integer: 'number', boolean: 'yes/no', date: 'date', datetime: 'date/time', decimal: 'decimal', float: 'decimal' };
|
|
138
|
+
function colTypeLabel(type) { return typeLabels[type] || type; }
|
|
139
|
+
|
|
140
|
+
function operatorsForType(type) {
|
|
141
|
+
switch(type) {
|
|
142
|
+
case 'boolean': return ['=', '!=', 'IS NULL', 'IS NOT NULL'];
|
|
143
|
+
case 'date': case 'datetime': return ['=', '!=', '>', '<', '>=', '<=', 'BETWEEN', 'IS NULL', 'IS NOT NULL'];
|
|
144
|
+
case 'integer': case 'decimal': case 'float': return ['=', '!=', '>', '<', '>=', '<=', 'IS NULL', 'IS NOT NULL'];
|
|
145
|
+
default: return ['=', '!=', 'LIKE', '>', '<', 'IS NULL', 'IS NOT NULL'];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function loadColumnsForTable(table, callback) {
|
|
150
|
+
el('vb-columns').innerHTML = '';
|
|
151
|
+
el('vb-filters').innerHTML = '';
|
|
152
|
+
el('vb-orders').innerHTML = '';
|
|
153
|
+
columnTypeMap = {};
|
|
154
|
+
if (!table) {
|
|
155
|
+
hide(el('vb-columns-section')); hide(el('vb-filters-section'));
|
|
156
|
+
hide(el('vb-order-section')); hide(el('vb-generated-sql'));
|
|
157
|
+
el('vb-run').disabled = true; el('vb-explain').disabled = true;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
ajaxGet(ROUTES.columns, { table: table }, function(cols) {
|
|
161
|
+
currentColumns = cols;
|
|
162
|
+
cols.forEach(function(c) { columnTypeMap[c.name] = c.type; });
|
|
163
|
+
var html = '';
|
|
164
|
+
cols.forEach(function(col) {
|
|
165
|
+
var checked = col['default'] ? ' checked' : '';
|
|
166
|
+
html += '<label class="mg-check"><input type="checkbox" class="vb-col-check" value="' + escHtml(col.name) + '"' + checked +
|
|
167
|
+
' data-default="' + (col['default'] ? '1' : '0') + '">' + escHtml(col.name) +
|
|
168
|
+
' <span class="type-hint">(' + colTypeLabel(col.type) + ')</span></label>';
|
|
169
|
+
});
|
|
170
|
+
el('vb-columns').innerHTML = html;
|
|
171
|
+
show(el('vb-columns-section')); show(el('vb-filters-section'));
|
|
172
|
+
show(el('vb-order-section')); show(el('vb-generated-sql'));
|
|
173
|
+
el('vb-run').disabled = false; el('vb-explain').disabled = false;
|
|
174
|
+
if (callback) callback(cols); else updateGeneratedSql();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
el('vb-table').addEventListener('change', function() { loadColumnsForTable(this.value); });
|
|
179
|
+
|
|
180
|
+
el('vb-toggle-all').addEventListener('click', function() {
|
|
181
|
+
var checks = qsa('.vb-col-check');
|
|
182
|
+
var allChecked = checks.every(function(c) { return c.checked; });
|
|
183
|
+
checks.forEach(function(c) { c.checked = !allChecked; });
|
|
184
|
+
updateGeneratedSql();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
el('vb-show-defaults').addEventListener('click', function() {
|
|
188
|
+
qsa('.vb-col-check').forEach(function(c) { c.checked = c.dataset['default'] === '1'; });
|
|
189
|
+
updateGeneratedSql();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
document.addEventListener('change', function(e) {
|
|
193
|
+
if (e.target.classList.contains('vb-col-check')) updateGeneratedSql();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function columnOptions(useAll) {
|
|
197
|
+
var cols = useAll ? currentColumns : currentColumns.filter(function(c) { return c['default']; });
|
|
198
|
+
var html = '<option value="">-- column --</option>';
|
|
199
|
+
cols.forEach(function(c) { html += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + ' (' + colTypeLabel(c.type) + ')</option>'; });
|
|
200
|
+
return html;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function addFilterRow(useAll) {
|
|
204
|
+
var div = document.createElement('div');
|
|
205
|
+
div.className = 'mg-row mg-mb vb-filter-row';
|
|
206
|
+
div.style.alignItems = 'center';
|
|
207
|
+
div.innerHTML =
|
|
208
|
+
'<div class="mg-col-3"><select class="vb-filter-col">' + columnOptions(useAll) + '</select></div>' +
|
|
209
|
+
'<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>' +
|
|
210
|
+
'<div class="mg-col-4 vb-filter-val-container"><input type="text" class="vb-filter-val" placeholder="value"></div>' +
|
|
211
|
+
'<div><button class="mg-btn mg-btn-outline-danger mg-btn-sm vb-remove-filter">✕</button></div>';
|
|
212
|
+
el('vb-filters').appendChild(div);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
el('vb-add-filter').addEventListener('click', function() { addFilterRow(false); updateGeneratedSql(); });
|
|
216
|
+
|
|
217
|
+
document.addEventListener('change', function(e) {
|
|
218
|
+
if (e.target.classList.contains('vb-filter-col')) {
|
|
219
|
+
var row = e.target.closest('.vb-filter-row');
|
|
220
|
+
var colType = columnTypeMap[e.target.value] || 'string';
|
|
221
|
+
var opSel = qs('.vb-filter-op', row);
|
|
222
|
+
var ops = operatorsForType(colType);
|
|
223
|
+
opSel.innerHTML = ops.map(function(o) { return '<option>' + o + '</option>'; }).join('');
|
|
224
|
+
updateValueInput(row, colType, opSel.value);
|
|
225
|
+
updateGeneratedSql();
|
|
226
|
+
}
|
|
227
|
+
if (e.target.classList.contains('vb-filter-op')) {
|
|
228
|
+
var row = e.target.closest('.vb-filter-row');
|
|
229
|
+
var colName = qs('.vb-filter-col', row).value;
|
|
230
|
+
var colType = columnTypeMap[colName] || 'string';
|
|
231
|
+
updateValueInput(row, colType, e.target.value);
|
|
232
|
+
updateGeneratedSql();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
document.addEventListener('input', function(e) {
|
|
237
|
+
if (e.target.classList.contains('vb-filter-val')) updateGeneratedSql();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
document.addEventListener('click', function(e) {
|
|
241
|
+
if (e.target.closest('.vb-remove-filter')) { e.target.closest('.vb-filter-row').remove(); updateGeneratedSql(); }
|
|
242
|
+
if (e.target.closest('.vb-remove-order')) { e.target.closest('.vb-order-row').remove(); updateGeneratedSql(); }
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
function updateValueInput(row, colType, op) {
|
|
246
|
+
var container = qs('.vb-filter-val-container', row);
|
|
247
|
+
if (op === 'IS NULL' || op === 'IS NOT NULL') { container.innerHTML = ''; return; }
|
|
248
|
+
if (colType === 'boolean') {
|
|
249
|
+
container.innerHTML = '<select class="vb-filter-val"><option value="1">True</option><option value="0">False</option></select>';
|
|
250
|
+
} else if ((colType === 'date' || colType === 'datetime') && op === 'BETWEEN') {
|
|
251
|
+
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;">';
|
|
252
|
+
} else if (colType === 'date' || colType === 'datetime') {
|
|
253
|
+
container.innerHTML = '<input type="date" class="vb-filter-val">';
|
|
254
|
+
} else if (colType === 'integer') {
|
|
255
|
+
container.innerHTML = '<input type="number" class="vb-filter-val" placeholder="number" step="1">';
|
|
256
|
+
} else if (colType === 'decimal' || colType === 'float') {
|
|
257
|
+
container.innerHTML = '<input type="number" class="vb-filter-val" placeholder="number" step="any">';
|
|
258
|
+
} else {
|
|
259
|
+
var ph = (op === 'LIKE') ? 'use % as wildcard' : 'value';
|
|
260
|
+
container.innerHTML = '<input type="text" class="vb-filter-val" placeholder="' + ph + '">';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- Order By ---
|
|
265
|
+
|
|
266
|
+
function addOrderRow(useAll) {
|
|
267
|
+
var cols = useAll ? currentColumns : currentColumns.filter(function(c) { return c['default']; });
|
|
268
|
+
var options = '<option value="">-- column --</option>';
|
|
269
|
+
cols.forEach(function(c) { options += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + '</option>'; });
|
|
270
|
+
var div = document.createElement('div');
|
|
271
|
+
div.className = 'mg-row mg-mb vb-order-row';
|
|
272
|
+
div.style.alignItems = 'center';
|
|
273
|
+
div.innerHTML =
|
|
274
|
+
'<div class="mg-col-3"><select class="vb-order-col">' + options + '</select></div>' +
|
|
275
|
+
'<div class="mg-col-2"><select class="vb-order-dir"><option value="ASC">Ascending</option><option value="DESC">Descending</option></select></div>' +
|
|
276
|
+
'<div><button class="mg-btn mg-btn-outline-danger mg-btn-sm vb-remove-order">✕</button></div>';
|
|
277
|
+
el('vb-orders').appendChild(div);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
el('vb-add-order').addEventListener('click', function() { addOrderRow(false); updateGeneratedSql(); });
|
|
281
|
+
|
|
282
|
+
document.addEventListener('change', function(e) {
|
|
283
|
+
if (e.target.classList.contains('vb-order-col') || e.target.classList.contains('vb-order-dir')) updateGeneratedSql();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// --- Build SQL ---
|
|
287
|
+
|
|
288
|
+
function buildSql() {
|
|
289
|
+
var table = el('vb-table').value;
|
|
290
|
+
if (!table) return '';
|
|
291
|
+
var cols = [];
|
|
292
|
+
qsa('.vb-col-check:checked').forEach(function(c) { cols.push('`' + c.value + '`'); });
|
|
293
|
+
if (!cols.length) cols = ['*'];
|
|
294
|
+
var sql = 'SELECT ' + cols.join(', ') + ' FROM `' + table + '`';
|
|
295
|
+
var wheres = [];
|
|
296
|
+
qsa('.vb-filter-row').forEach(function(row) {
|
|
297
|
+
var col = qs('.vb-filter-col', row).value;
|
|
298
|
+
var op = qs('.vb-filter-op', row).value;
|
|
299
|
+
var valEl = qs('.vb-filter-val', row);
|
|
300
|
+
var val = valEl ? valEl.value : '';
|
|
301
|
+
if (!col) return;
|
|
302
|
+
if (op === 'IS NULL' || op === 'IS NOT NULL') {
|
|
303
|
+
wheres.push('`' + col + '` ' + op);
|
|
304
|
+
} else if (op === 'BETWEEN') {
|
|
305
|
+
var endEl = qs('.vb-filter-val-end', row);
|
|
306
|
+
var endVal = endEl ? endEl.value : '';
|
|
307
|
+
wheres.push("`" + col + "` BETWEEN '" + val.replace(/'/g, "''") + "' AND '" + endVal.replace(/'/g, "''") + "'");
|
|
308
|
+
} else if (op === 'LIKE') {
|
|
309
|
+
wheres.push("`" + col + "` LIKE '" + val.replace(/'/g, "''") + "'");
|
|
310
|
+
} else {
|
|
311
|
+
wheres.push("`" + col + "` " + op + " '" + val.replace(/'/g, "''") + "'");
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
if (wheres.length) sql += ' WHERE ' + wheres.join(' AND ');
|
|
315
|
+
var orders = [];
|
|
316
|
+
qsa('.vb-order-row').forEach(function(row) {
|
|
317
|
+
var col = qs('.vb-order-col', row).value;
|
|
318
|
+
var dir = qs('.vb-order-dir', row).value;
|
|
319
|
+
if (col) orders.push('`' + col + '` ' + dir);
|
|
320
|
+
});
|
|
321
|
+
if (orders.length) sql += ' ORDER BY ' + orders.join(', ');
|
|
322
|
+
return sql;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function updateGeneratedSql() {
|
|
326
|
+
var sql = buildSql();
|
|
327
|
+
el('vb-sql-preview').value = sql;
|
|
328
|
+
el('sql-input').value = sql;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- Run Query ---
|
|
332
|
+
|
|
333
|
+
el('vb-run').addEventListener('click', function() {
|
|
334
|
+
var sql = buildSql();
|
|
335
|
+
if (sql) runQuery(sql, parseInt(el('vb-row-limit').value) || 25);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
el('sql-run').addEventListener('click', function() {
|
|
339
|
+
var sql = el('sql-input').value.trim();
|
|
340
|
+
if (sql) runQuery(sql, parseInt(el('sql-row-limit').value) || 25);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
el('sql-input').addEventListener('input', function() { el('vb-sql-preview').value = this.value; });
|
|
344
|
+
|
|
345
|
+
function runQuery(sql, rowLimit) {
|
|
346
|
+
clearResults();
|
|
347
|
+
setBtnLoading(['vb-run', 'sql-run'], true);
|
|
348
|
+
ajax('POST', ROUTES.execute, { sql: sql, row_limit: rowLimit }, function(data) {
|
|
349
|
+
renderResults(data);
|
|
350
|
+
setBtnLoading(['vb-run', 'sql-run'], false);
|
|
351
|
+
}, function(json) {
|
|
352
|
+
var cls = (json && json.timeout) ? 'mg-alert-warning' : 'mg-alert-danger';
|
|
353
|
+
el('results-alert').innerHTML = '<div class="mg-alert ' + cls + '">' + escHtml((json && json.error) || 'An unexpected error occurred.') + '</div>';
|
|
354
|
+
show(el('results-alert'));
|
|
355
|
+
setBtnLoading(['vb-run', 'sql-run'], false);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function clearResults() {
|
|
360
|
+
hide(el('results-alert')); hide(el('results-stats'));
|
|
361
|
+
hide(el('results-table-wrapper')); hide(el('results-empty'));
|
|
362
|
+
hide(el('results-truncated'));
|
|
363
|
+
el('results-thead').innerHTML = '';
|
|
364
|
+
el('results-tbody').innerHTML = '';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function renderResults(data) {
|
|
368
|
+
if (data.row_count === 0) {
|
|
369
|
+
show(el('results-empty')); show(el('results-stats'));
|
|
370
|
+
el('results-row-count').textContent = '0 rows';
|
|
371
|
+
el('results-time').textContent = data.execution_time_ms + ' ms';
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
el('results-row-count').textContent = data.row_count + ' row' + (data.row_count !== 1 ? 's' : '');
|
|
375
|
+
el('results-time').textContent = data.execution_time_ms + ' ms';
|
|
376
|
+
if (data.truncated) show(el('results-truncated'));
|
|
377
|
+
show(el('results-stats'));
|
|
378
|
+
|
|
379
|
+
el('results-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
|
|
380
|
+
el('results-tbody').innerHTML = data.rows.map(function(row) {
|
|
381
|
+
return '<tr>' + row.map(function(val) {
|
|
382
|
+
if (val === null) return '<td><em class="null">NULL</em></td>';
|
|
383
|
+
if (val === '[REDACTED]') return '<td><span class="redacted">[REDACTED]</span></td>';
|
|
384
|
+
return '<td>' + escHtml(String(val)) + '</td>';
|
|
385
|
+
}).join('') + '</tr>';
|
|
386
|
+
}).join('');
|
|
387
|
+
show(el('results-table-wrapper'));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- Explain ---
|
|
391
|
+
|
|
392
|
+
el('vb-explain').addEventListener('click', function() { var sql = buildSql(); if (sql) runExplain(sql); });
|
|
393
|
+
el('sql-explain').addEventListener('click', function() { var sql = el('sql-input').value.trim(); if (sql) runExplain(sql); });
|
|
394
|
+
el('explain-close').addEventListener('click', function() { hide(el('explain-results')); });
|
|
395
|
+
|
|
396
|
+
function runExplain(sql, fromSlowQuery) {
|
|
397
|
+
lastExplainSql = sql;
|
|
398
|
+
hide(el('explain-results')); hide(el('optimize-results'));
|
|
399
|
+
el('explain-thead').innerHTML = '';
|
|
400
|
+
el('explain-tbody').innerHTML = '';
|
|
401
|
+
setBtnLoading(['vb-explain', 'sql-explain'], true);
|
|
402
|
+
|
|
403
|
+
var postData = { sql: sql };
|
|
404
|
+
if (fromSlowQuery) postData.from_slow_query = 'true';
|
|
405
|
+
ajax('POST', ROUTES.explain, postData, function(data) {
|
|
406
|
+
lastExplainRows = data.rows;
|
|
407
|
+
el('explain-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
|
|
408
|
+
el('explain-tbody').innerHTML = data.rows.map(function(row) {
|
|
409
|
+
return '<tr>' + row.map(function(val) {
|
|
410
|
+
return val === null ? '<td><em class="null">NULL</em></td>' : '<td>' + escHtml(String(val)) + '</td>';
|
|
411
|
+
}).join('') + '</tr>';
|
|
412
|
+
}).join('');
|
|
413
|
+
show(el('explain-results'));
|
|
414
|
+
setBtnLoading(['vb-explain', 'sql-explain'], false);
|
|
415
|
+
}, function(json) {
|
|
416
|
+
el('results-alert').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml((json && json.error) || 'Explain failed.') + '</div>';
|
|
417
|
+
show(el('results-alert'));
|
|
418
|
+
setBtnLoading(['vb-explain', 'sql-explain'], false);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- AI ---
|
|
423
|
+
|
|
424
|
+
var aiToggle = el('ai-toggle');
|
|
425
|
+
if (aiToggle) {
|
|
426
|
+
aiToggle.addEventListener('click', function() {
|
|
427
|
+
var panel = el('ai-panel');
|
|
428
|
+
panel.classList.toggle('mg-hidden');
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
var aiSuggest = el('ai-suggest');
|
|
433
|
+
if (aiSuggest) {
|
|
434
|
+
aiSuggest.addEventListener('click', function() {
|
|
435
|
+
var prompt = el('ai-prompt').value.trim();
|
|
436
|
+
if (!prompt) return;
|
|
437
|
+
aiSuggest.disabled = true;
|
|
438
|
+
aiSuggest.innerHTML = '<span class="mg-spinner"></span> Thinking...';
|
|
439
|
+
hide(el('ai-result'));
|
|
440
|
+
|
|
441
|
+
ajax('POST', ROUTES.suggest, { prompt: prompt }, function(data) {
|
|
442
|
+
el('sql-input').value = data.sql || '';
|
|
443
|
+
if (data.explanation) {
|
|
444
|
+
el('ai-explanation').textContent = data.explanation;
|
|
445
|
+
show(el('ai-result'));
|
|
446
|
+
}
|
|
447
|
+
parseSqlToBuilder(data.sql);
|
|
448
|
+
aiSuggest.disabled = false;
|
|
449
|
+
aiSuggest.innerHTML = '⚡ Suggest Query';
|
|
450
|
+
}, function(json) {
|
|
451
|
+
el('results-alert').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml((json && json.error) || 'AI suggestion failed.') + '</div>';
|
|
452
|
+
show(el('results-alert'));
|
|
453
|
+
aiSuggest.disabled = false;
|
|
454
|
+
aiSuggest.innerHTML = '⚡ Suggest Query';
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
var explainOptimize = el('explain-optimize');
|
|
460
|
+
if (explainOptimize) {
|
|
461
|
+
explainOptimize.addEventListener('click', function() {
|
|
462
|
+
explainOptimize.disabled = true;
|
|
463
|
+
explainOptimize.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
464
|
+
hide(el('optimize-results'));
|
|
465
|
+
|
|
466
|
+
var data = { sql: lastExplainSql };
|
|
467
|
+
lastExplainRows.forEach(function(row, i) {
|
|
468
|
+
row.forEach(function(val, j) {
|
|
469
|
+
data['explain_rows[' + i + '][' + j + ']'] = val == null ? '' : val;
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
ajax('POST', ROUTES.optimize, data, function(resp) {
|
|
474
|
+
el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.');
|
|
475
|
+
show(el('optimize-results'));
|
|
476
|
+
explainOptimize.disabled = false;
|
|
477
|
+
explainOptimize.innerHTML = '⚡ AI Optimization';
|
|
478
|
+
}, function(json) {
|
|
479
|
+
el('optimize-content').textContent = (json && json.error) || 'Optimization failed.';
|
|
480
|
+
show(el('optimize-results'));
|
|
481
|
+
explainOptimize.disabled = false;
|
|
482
|
+
explainOptimize.innerHTML = '⚡ AI Optimization';
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// --- Slow Queries ---
|
|
488
|
+
|
|
489
|
+
function loadSlowQueries() {
|
|
490
|
+
show(el('slow-loading'));
|
|
491
|
+
hide(el('slow-empty')); hide(el('slow-table-wrapper'));
|
|
492
|
+
el('slow-tbody').innerHTML = '';
|
|
493
|
+
|
|
494
|
+
ajaxGet(ROUTES.slow_queries, {}, function(data) {
|
|
495
|
+
hide(el('slow-loading'));
|
|
496
|
+
if (!data || !data.length) { show(el('slow-empty')); el('slow-count').textContent = '0'; return; }
|
|
497
|
+
el('slow-count').textContent = data.length + ' queries';
|
|
498
|
+
el('slow-tbody').innerHTML = data.map(function(q) {
|
|
499
|
+
var d = q.duration_ms;
|
|
500
|
+
var cls = d >= 2000 ? 'mg-badge-danger' : d >= 1000 ? 'mg-badge-warning' : 'mg-badge-info';
|
|
501
|
+
var sqlEsc = escHtml(q.sql);
|
|
502
|
+
var sqlShort = sqlEsc.length > 200 ? sqlEsc.substring(0, 200) + '...' : sqlEsc;
|
|
503
|
+
return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
|
|
504
|
+
'<td><small>' + escHtml(q.timestamp) + '</small></td>' +
|
|
505
|
+
'<td><code title="' + sqlEsc + '">' + sqlShort + '</code></td>' +
|
|
506
|
+
'<td><button class="mg-btn mg-btn-outline mg-btn-sm slow-explain-btn" data-sql="' + sqlEsc.replace(/"/g, '"') + '">Explain</button> ' +
|
|
507
|
+
'<button class="mg-btn mg-btn-outline-secondary mg-btn-sm slow-use-btn" data-sql="' + sqlEsc.replace(/"/g, '"') + '">Use</button></td></tr>';
|
|
508
|
+
}).join('');
|
|
509
|
+
show(el('slow-table-wrapper'));
|
|
510
|
+
}, function() {
|
|
511
|
+
hide(el('slow-loading'));
|
|
512
|
+
el('slow-empty').textContent = 'Failed to load slow queries.';
|
|
513
|
+
show(el('slow-empty'));
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
el('slow-refresh').addEventListener('click', loadSlowQueries);
|
|
518
|
+
|
|
519
|
+
document.addEventListener('click', function(e) {
|
|
520
|
+
var btn = e.target.closest('.slow-explain-btn');
|
|
521
|
+
if (btn) runExplain(btn.dataset.sql, true);
|
|
522
|
+
var useBtn = e.target.closest('.slow-use-btn');
|
|
523
|
+
if (useBtn) {
|
|
524
|
+
el('sql-input').value = useBtn.dataset.sql;
|
|
525
|
+
qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
526
|
+
qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
527
|
+
qs('[data-tab="sql"]').classList.add('active');
|
|
528
|
+
el('tab-sql').classList.add('active');
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// --- Duplicate Indexes ---
|
|
533
|
+
|
|
534
|
+
function loadDuplicateIndexes() {
|
|
535
|
+
show(el('dup-loading'));
|
|
536
|
+
hide(el('dup-empty')); hide(el('dup-table-wrapper'));
|
|
537
|
+
el('dup-tbody').innerHTML = '';
|
|
538
|
+
|
|
539
|
+
ajaxGet(ROUTES.duplicate_indexes, {}, function(data) {
|
|
540
|
+
hide(el('dup-loading'));
|
|
541
|
+
if (!data || !data.length) { show(el('dup-empty')); el('dup-count').textContent = '0'; return; }
|
|
542
|
+
el('dup-count').textContent = data.length + ' found';
|
|
543
|
+
el('dup-tbody').innerHTML = data.map(function(d) {
|
|
544
|
+
var dropSql = 'ALTER TABLE `' + d.table + '` DROP INDEX `' + d.duplicate_index + '`;';
|
|
545
|
+
return '<tr>' +
|
|
546
|
+
'<td><strong>' + escHtml(d.table) + '</strong></td>' +
|
|
547
|
+
'<td><code>' + escHtml(d.duplicate_index) + '</code>' + (d.unique ? ' <span class="mg-badge mg-badge-warning">UNIQUE</span>' : '') + '</td>' +
|
|
548
|
+
'<td>' + d.duplicate_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
|
|
549
|
+
'<td><code>' + escHtml(d.covered_by_index) + '</code></td>' +
|
|
550
|
+
'<td>' + d.covered_by_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
|
|
551
|
+
'<td><code style="font-size:11px;user-select:all;cursor:pointer;" title="Click to select">' + escHtml(dropSql) + '</code></td>' +
|
|
552
|
+
'</tr>';
|
|
553
|
+
}).join('');
|
|
554
|
+
show(el('dup-table-wrapper'));
|
|
555
|
+
}, function() {
|
|
556
|
+
hide(el('dup-loading'));
|
|
557
|
+
el('dup-empty').textContent = 'Failed to scan indexes.';
|
|
558
|
+
show(el('dup-empty'));
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
el('dup-refresh').addEventListener('click', loadDuplicateIndexes);
|
|
563
|
+
|
|
564
|
+
// --- Table Sizes ---
|
|
565
|
+
|
|
566
|
+
function formatMb(mb) {
|
|
567
|
+
if (mb >= 1024) return (mb / 1024).toFixed(2) + ' GB';
|
|
568
|
+
if (mb >= 1) return mb.toFixed(2) + ' MB';
|
|
569
|
+
return (mb * 1024).toFixed(0) + ' KB';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function sizeBar(pct, color) {
|
|
573
|
+
return '<div style="background:#e9ecef;border-radius:3px;height:8px;width:100%;">' +
|
|
574
|
+
'<div style="background:' + color + ';border-radius:3px;height:8px;width:' + Math.max(pct, 1) + '%;"></div></div>';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
var PIE_COLORS = [
|
|
578
|
+
'#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f',
|
|
579
|
+
'#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac',
|
|
580
|
+
'#86bcb6', '#8cd17d', '#b6992d', '#499894', '#d37295'
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
function drawPieChart(canvas, slices, legendEl) {
|
|
584
|
+
var ctx = canvas.getContext('2d');
|
|
585
|
+
var w = canvas.width;
|
|
586
|
+
var h = canvas.height;
|
|
587
|
+
var cx = w / 2;
|
|
588
|
+
var cy = h / 2;
|
|
589
|
+
var r = Math.min(cx, cy) - 4;
|
|
590
|
+
var total = slices.reduce(function(s, sl) { return s + sl.value; }, 0);
|
|
591
|
+
if (total === 0) return;
|
|
592
|
+
|
|
593
|
+
ctx.clearRect(0, 0, w, h);
|
|
594
|
+
var startAngle = -Math.PI / 2;
|
|
595
|
+
|
|
596
|
+
slices.forEach(function(sl, i) {
|
|
597
|
+
var sliceAngle = (sl.value / total) * 2 * Math.PI;
|
|
598
|
+
ctx.beginPath();
|
|
599
|
+
ctx.moveTo(cx, cy);
|
|
600
|
+
ctx.arc(cx, cy, r, startAngle, startAngle + sliceAngle);
|
|
601
|
+
ctx.closePath();
|
|
602
|
+
ctx.fillStyle = PIE_COLORS[i % PIE_COLORS.length];
|
|
603
|
+
ctx.fill();
|
|
604
|
+
ctx.strokeStyle = '#fff';
|
|
605
|
+
ctx.lineWidth = 1.5;
|
|
606
|
+
ctx.stroke();
|
|
607
|
+
startAngle += sliceAngle;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Legend
|
|
611
|
+
legendEl.innerHTML = slices.map(function(sl, i) {
|
|
612
|
+
var pct = ((sl.value / total) * 100).toFixed(1);
|
|
613
|
+
return '<div style="display:flex;align-items:center;gap:6px;">' +
|
|
614
|
+
'<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + PIE_COLORS[i % PIE_COLORS.length] + ';flex-shrink:0;"></span>' +
|
|
615
|
+
'<span>' + escHtml(sl.label) + '</span>' +
|
|
616
|
+
'<span class="mg-text-muted">' + formatMb(sl.value) + ' (' + pct + '%)</span></div>';
|
|
617
|
+
}).join('');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function loadTableSizes() {
|
|
621
|
+
show(el('sizes-loading'));
|
|
622
|
+
hide(el('sizes-table-wrapper')); hide(el('sizes-chart-wrapper'));
|
|
623
|
+
el('sizes-tbody').innerHTML = '';
|
|
624
|
+
|
|
625
|
+
ajaxGet(ROUTES.table_sizes, {}, function(data) {
|
|
626
|
+
hide(el('sizes-loading'));
|
|
627
|
+
if (!data || !data.length) { el('sizes-total').textContent = '0 tables'; show(el('sizes-table-wrapper')); return; }
|
|
628
|
+
|
|
629
|
+
var totalMb = data.reduce(function(sum, t) { return sum + t.total_mb; }, 0);
|
|
630
|
+
el('sizes-total').textContent = data.length + ' tables, ' + formatMb(totalMb) + ' total';
|
|
631
|
+
var maxMb = data[0].total_mb || 1;
|
|
632
|
+
|
|
633
|
+
// Pie chart: top 10 tables + "Other"
|
|
634
|
+
var topN = data.slice(0, 10);
|
|
635
|
+
var otherMb = data.slice(10).reduce(function(s, t) { return s + t.total_mb; }, 0);
|
|
636
|
+
var slices = topN.map(function(t) { return { label: t.table, value: t.total_mb }; });
|
|
637
|
+
if (otherMb > 0) slices.push({ label: 'Other (' + (data.length - 10) + ' tables)', value: otherMb });
|
|
638
|
+
drawPieChart(el('sizes-pie'), slices, el('sizes-legend'));
|
|
639
|
+
show(el('sizes-chart-wrapper'));
|
|
640
|
+
|
|
641
|
+
el('sizes-tbody').innerHTML = data.map(function(t) {
|
|
642
|
+
var pct = (t.total_mb / maxMb) * 100;
|
|
643
|
+
var color = t.total_mb >= 100 ? '#dc3545' : t.total_mb >= 10 ? '#ffc107' : '#28a745';
|
|
644
|
+
var rows = t.rows != null ? Number(t.rows).toLocaleString() : '?';
|
|
645
|
+
return '<tr>' +
|
|
646
|
+
'<td><strong>' + escHtml(t.table) + '</strong></td>' +
|
|
647
|
+
'<td style="text-align:right">' + rows + '</td>' +
|
|
648
|
+
'<td style="text-align:right">' + formatMb(t.data_mb) + '</td>' +
|
|
649
|
+
'<td style="text-align:right">' + formatMb(t.index_mb) + '</td>' +
|
|
650
|
+
'<td style="text-align:right"><strong>' + formatMb(t.total_mb) + '</strong></td>' +
|
|
651
|
+
'<td style="text-align:right">' + (t.fragmented_mb > 0 ? formatMb(t.fragmented_mb) : '-') + '</td>' +
|
|
652
|
+
'<td>' + sizeBar(pct, color) + '</td>' +
|
|
653
|
+
'</tr>';
|
|
654
|
+
}).join('');
|
|
655
|
+
show(el('sizes-table-wrapper'));
|
|
656
|
+
}, function() {
|
|
657
|
+
hide(el('sizes-loading'));
|
|
658
|
+
el('sizes-total').textContent = 'Failed to load';
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
el('sizes-refresh').addEventListener('click', loadTableSizes);
|
|
663
|
+
|
|
664
|
+
// --- Query Stats ---
|
|
665
|
+
|
|
666
|
+
function loadQueryStats() {
|
|
667
|
+
show(el('qstats-loading'));
|
|
668
|
+
hide(el('qstats-empty')); hide(el('qstats-table-wrapper')); hide(el('qstats-error'));
|
|
669
|
+
el('qstats-tbody').innerHTML = '';
|
|
670
|
+
|
|
671
|
+
var sort = el('qstats-sort').value;
|
|
672
|
+
ajaxGet(ROUTES.query_stats, { sort: sort }, function(data) {
|
|
673
|
+
hide(el('qstats-loading'));
|
|
674
|
+
if (data.error) {
|
|
675
|
+
el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
|
|
676
|
+
show(el('qstats-error')); return;
|
|
677
|
+
}
|
|
678
|
+
if (!data.length) { show(el('qstats-empty')); el('qstats-count').textContent = '0'; return; }
|
|
679
|
+
el('qstats-count').textContent = data.length + ' queries';
|
|
680
|
+
el('qstats-tbody').innerHTML = data.map(function(q) {
|
|
681
|
+
var ratioClass = q.rows_ratio > 100 ? 'mg-badge-danger' : q.rows_ratio > 10 ? 'mg-badge-warning' : '';
|
|
682
|
+
var sqlShort = q.sql.length > 120 ? q.sql.substring(0, 120) + '...' : q.sql;
|
|
683
|
+
return '<tr>' +
|
|
684
|
+
'<td><code style="font-size:11px;word-break:break-all;" title="' + escHtml(q.sql) + '">' + escHtml(sqlShort) + '</code></td>' +
|
|
685
|
+
'<td style="text-align:right">' + Number(q.calls).toLocaleString() + '</td>' +
|
|
686
|
+
'<td style="text-align:right">' + formatDuration(q.total_time_ms) + '</td>' +
|
|
687
|
+
'<td style="text-align:right">' + formatDuration(q.avg_time_ms) + '</td>' +
|
|
688
|
+
'<td style="text-align:right">' + formatDuration(q.max_time_ms) + '</td>' +
|
|
689
|
+
'<td style="text-align:right">' + Number(q.rows_examined).toLocaleString() + '</td>' +
|
|
690
|
+
'<td style="text-align:right">' + Number(q.rows_sent).toLocaleString() + '</td>' +
|
|
691
|
+
'<td style="text-align:right">' + (ratioClass ? '<span class="mg-badge ' + ratioClass + '">' + q.rows_ratio + 'x</span>' : q.rows_ratio + 'x') + '</td>' +
|
|
692
|
+
'</tr>';
|
|
693
|
+
}).join('');
|
|
694
|
+
show(el('qstats-table-wrapper'));
|
|
695
|
+
}, function(json) {
|
|
696
|
+
hide(el('qstats-loading'));
|
|
697
|
+
el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load query stats.') + '</div>';
|
|
698
|
+
show(el('qstats-error'));
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function formatDuration(ms) {
|
|
703
|
+
if (ms >= 60000) return (ms / 60000).toFixed(1) + ' min';
|
|
704
|
+
if (ms >= 1000) return (ms / 1000).toFixed(1) + ' s';
|
|
705
|
+
return ms.toFixed(1) + ' ms';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
el('qstats-refresh').addEventListener('click', loadQueryStats);
|
|
709
|
+
el('qstats-sort').addEventListener('change', loadQueryStats);
|
|
710
|
+
|
|
711
|
+
// --- Unused Indexes ---
|
|
712
|
+
|
|
713
|
+
function loadUnusedIndexes() {
|
|
714
|
+
show(el('unused-loading'));
|
|
715
|
+
hide(el('unused-empty')); hide(el('unused-table-wrapper')); hide(el('unused-error'));
|
|
716
|
+
el('unused-tbody').innerHTML = '';
|
|
717
|
+
|
|
718
|
+
ajaxGet(ROUTES.unused_indexes, {}, function(data) {
|
|
719
|
+
hide(el('unused-loading'));
|
|
720
|
+
if (data.error) {
|
|
721
|
+
el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
|
|
722
|
+
show(el('unused-error')); return;
|
|
723
|
+
}
|
|
724
|
+
if (!data.length) { show(el('unused-empty')); el('unused-count').textContent = '0'; return; }
|
|
725
|
+
el('unused-count').textContent = data.length + ' found';
|
|
726
|
+
el('unused-tbody').innerHTML = data.map(function(d) {
|
|
727
|
+
return '<tr>' +
|
|
728
|
+
'<td><strong>' + escHtml(d.table) + '</strong></td>' +
|
|
729
|
+
'<td><code>' + escHtml(d.index_name) + '</code></td>' +
|
|
730
|
+
'<td style="text-align:right">' + d.reads + '</td>' +
|
|
731
|
+
'<td style="text-align:right">' + Number(d.writes).toLocaleString() + '</td>' +
|
|
732
|
+
'<td style="text-align:right">' + Number(d.table_rows).toLocaleString() + '</td>' +
|
|
733
|
+
'<td><code style="font-size:11px;user-select:all;cursor:pointer;" title="Click to select">' + escHtml(d.drop_sql) + '</code></td>' +
|
|
734
|
+
'</tr>';
|
|
735
|
+
}).join('');
|
|
736
|
+
show(el('unused-table-wrapper'));
|
|
737
|
+
}, function(json) {
|
|
738
|
+
hide(el('unused-loading'));
|
|
739
|
+
el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load unused indexes.') + '</div>';
|
|
740
|
+
show(el('unused-error'));
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
el('unused-refresh').addEventListener('click', loadUnusedIndexes);
|
|
745
|
+
|
|
746
|
+
// --- Server Overview ---
|
|
747
|
+
|
|
748
|
+
function usageBar(pct, label) {
|
|
749
|
+
var color = pct >= 90 ? '#dc3545' : pct >= 70 ? '#ffc107' : '#28a745';
|
|
750
|
+
return '<div class="mg-usage-bar">' +
|
|
751
|
+
'<div class="mg-usage-bar-fill" style="width:' + Math.min(pct, 100) + '%;background:' + color + ';"></div>' +
|
|
752
|
+
'<div class="mg-usage-bar-text">' + label + '</div></div>';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function statRow(label, value) {
|
|
756
|
+
return '<div class="mg-stat-label">' + label + '</div><div class="mg-stat-value">' + value + '</div>';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function loadServerOverview() {
|
|
760
|
+
show(el('server-loading'));
|
|
761
|
+
hide(el('server-content')); hide(el('server-error'));
|
|
762
|
+
|
|
763
|
+
ajaxGet(ROUTES.server_overview, {}, function(data) {
|
|
764
|
+
hide(el('server-loading'));
|
|
765
|
+
if (data.error) {
|
|
766
|
+
el('server-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
|
|
767
|
+
show(el('server-error')); return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
var s = data.server;
|
|
771
|
+
var c = data.connections;
|
|
772
|
+
var db = data.innodb;
|
|
773
|
+
var q = data.queries;
|
|
774
|
+
|
|
775
|
+
// Server info
|
|
776
|
+
el('server-info').innerHTML =
|
|
777
|
+
statRow('Version', '<code>' + escHtml(s.version) + '</code>') +
|
|
778
|
+
statRow('Uptime', escHtml(s.uptime)) +
|
|
779
|
+
statRow('Queries/sec', q.qps) +
|
|
780
|
+
statRow('Total Queries', Number(q.questions).toLocaleString()) +
|
|
781
|
+
statRow('Slow Queries', Number(q.slow_queries).toLocaleString());
|
|
782
|
+
|
|
783
|
+
// Connections
|
|
784
|
+
el('conn-bar').innerHTML = usageBar(c.usage_pct, c.current + ' / ' + c.max + ' (' + c.usage_pct + '%)');
|
|
785
|
+
el('conn-info').innerHTML =
|
|
786
|
+
statRow('Threads Running', c.threads_running) +
|
|
787
|
+
statRow('Threads Cached', c.threads_cached) +
|
|
788
|
+
statRow('Threads Created', Number(c.threads_created).toLocaleString()) +
|
|
789
|
+
statRow('Max Used', c.max_used) +
|
|
790
|
+
statRow('Aborted Connects', Number(c.aborted_connects).toLocaleString()) +
|
|
791
|
+
statRow('Aborted Clients', Number(c.aborted_clients).toLocaleString());
|
|
792
|
+
|
|
793
|
+
// InnoDB
|
|
794
|
+
var poolUsedPct = db.buffer_pool_pages_total > 0
|
|
795
|
+
? (((db.buffer_pool_pages_total - db.buffer_pool_pages_free) / db.buffer_pool_pages_total) * 100).toFixed(1)
|
|
796
|
+
: 0;
|
|
797
|
+
el('innodb-bar').innerHTML = usageBar(parseFloat(poolUsedPct), db.buffer_pool_mb + ' MB (' + poolUsedPct + '% used)');
|
|
798
|
+
el('innodb-info').innerHTML =
|
|
799
|
+
statRow('Hit Rate', db.buffer_pool_hit_rate + '%') +
|
|
800
|
+
statRow('Dirty Pages', Number(db.buffer_pool_pages_dirty).toLocaleString()) +
|
|
801
|
+
statRow('Free Pages', Number(db.buffer_pool_pages_free).toLocaleString()) +
|
|
802
|
+
statRow('Row Lock Waits', Number(db.row_lock_waits).toLocaleString()) +
|
|
803
|
+
statRow('Row Lock Time', formatDuration(db.row_lock_time_ms));
|
|
804
|
+
|
|
805
|
+
// Query activity
|
|
806
|
+
var tmpBadge = q.tmp_disk_pct > 25
|
|
807
|
+
? '<span class="mg-badge mg-badge-danger">' + q.tmp_disk_pct + '%</span>'
|
|
808
|
+
: q.tmp_disk_pct + '%';
|
|
809
|
+
el('query-info').innerHTML =
|
|
810
|
+
statRow('Tmp Tables (disk)', Number(q.tmp_disk_tables).toLocaleString() + ' / ' + Number(q.tmp_tables).toLocaleString() + ' ' + tmpBadge) +
|
|
811
|
+
statRow('Full Joins (no index)', Number(q.select_full_join).toLocaleString()) +
|
|
812
|
+
statRow('Sort Merge Passes', Number(q.sort_merge_passes).toLocaleString());
|
|
813
|
+
|
|
814
|
+
show(el('server-content'));
|
|
815
|
+
}, function(json) {
|
|
816
|
+
hide(el('server-loading'));
|
|
817
|
+
el('server-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load server overview.') + '</div>';
|
|
818
|
+
show(el('server-error'));
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
el('server-refresh').addEventListener('click', loadServerOverview);
|
|
823
|
+
|
|
824
|
+
// --- AI Feature Handlers ---
|
|
825
|
+
|
|
826
|
+
// Generic AI call helper
|
|
827
|
+
function aiCall(url, data, onSuccess, onError) {
|
|
828
|
+
ajax('POST', url, data, function(result) {
|
|
829
|
+
if (result.error) { onError(result.error); return; }
|
|
830
|
+
onSuccess(result);
|
|
831
|
+
}, function(json) {
|
|
832
|
+
onError((json && json.error) || 'AI request failed.');
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function showAiQueryResult(title, html) {
|
|
837
|
+
el('ai-query-title').innerHTML = '<strong>⚡ ' + escHtml(title) + '</strong>';
|
|
838
|
+
el('ai-query-content').innerHTML = html;
|
|
839
|
+
show(el('ai-query-result'));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
el('ai-query-close').addEventListener('click', function() { hide(el('ai-query-result')); });
|
|
843
|
+
|
|
844
|
+
// Describe Query
|
|
845
|
+
var sqlDescribe = el('sql-describe');
|
|
846
|
+
if (sqlDescribe) {
|
|
847
|
+
sqlDescribe.addEventListener('click', function() {
|
|
848
|
+
var sql = el('sql-input').value.trim();
|
|
849
|
+
if (!sql) return;
|
|
850
|
+
sqlDescribe.disabled = true;
|
|
851
|
+
sqlDescribe.innerHTML = '<span class="mg-spinner"></span>';
|
|
852
|
+
hide(el('ai-query-result'));
|
|
853
|
+
aiCall(ROUTES.describe_query, { sql: sql }, function(data) {
|
|
854
|
+
showAiQueryResult('Query Description', formatMarkdown(data.explanation || data.raw || 'No explanation returned.'));
|
|
855
|
+
sqlDescribe.disabled = false;
|
|
856
|
+
sqlDescribe.innerHTML = '⚡ Describe';
|
|
857
|
+
}, function(err) {
|
|
858
|
+
showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
|
|
859
|
+
sqlDescribe.disabled = false;
|
|
860
|
+
sqlDescribe.innerHTML = '⚡ Describe';
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Rewrite Query
|
|
866
|
+
var sqlRewrite = el('sql-rewrite');
|
|
867
|
+
if (sqlRewrite) {
|
|
868
|
+
sqlRewrite.addEventListener('click', function() {
|
|
869
|
+
var sql = el('sql-input').value.trim();
|
|
870
|
+
if (!sql) return;
|
|
871
|
+
sqlRewrite.disabled = true;
|
|
872
|
+
sqlRewrite.innerHTML = '<span class="mg-spinner"></span>';
|
|
873
|
+
hide(el('ai-query-result'));
|
|
874
|
+
aiCall(ROUTES.rewrite_query, { sql: sql }, function(data) {
|
|
875
|
+
var html = '';
|
|
876
|
+
if (data.rewritten) {
|
|
877
|
+
html += '<strong>Rewritten Query:</strong><pre class="mg-pre"><code>' + escHtml(data.rewritten) + '</code></pre>';
|
|
878
|
+
}
|
|
879
|
+
if (data.changes) {
|
|
880
|
+
html += '<strong>Changes:</strong><br>' + formatMarkdown(data.changes);
|
|
881
|
+
}
|
|
882
|
+
if (!data.rewritten && !data.changes) {
|
|
883
|
+
html = formatMarkdown(data.raw || 'No rewrite suggestions.');
|
|
884
|
+
}
|
|
885
|
+
showAiQueryResult('Query Rewrite', html);
|
|
886
|
+
sqlRewrite.disabled = false;
|
|
887
|
+
sqlRewrite.innerHTML = '⚡ Rewrite';
|
|
888
|
+
}, function(err) {
|
|
889
|
+
showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
|
|
890
|
+
sqlRewrite.disabled = false;
|
|
891
|
+
sqlRewrite.innerHTML = '⚡ Rewrite';
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Index Advisor (on EXPLAIN results)
|
|
897
|
+
var indexAdvisor = el('explain-index-advisor');
|
|
898
|
+
if (indexAdvisor) {
|
|
899
|
+
indexAdvisor.addEventListener('click', function() {
|
|
900
|
+
indexAdvisor.disabled = true;
|
|
901
|
+
indexAdvisor.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
902
|
+
hide(el('optimize-results'));
|
|
903
|
+
|
|
904
|
+
var data = { sql: lastExplainSql };
|
|
905
|
+
lastExplainRows.forEach(function(row, i) {
|
|
906
|
+
row.forEach(function(val, j) {
|
|
907
|
+
data['explain_rows[' + i + '][' + j + ']'] = val == null ? '' : val;
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
aiCall(ROUTES.index_advisor, data, function(resp) {
|
|
912
|
+
el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.');
|
|
913
|
+
show(el('optimize-results'));
|
|
914
|
+
indexAdvisor.disabled = false;
|
|
915
|
+
indexAdvisor.innerHTML = '⚡ Index Advisor';
|
|
916
|
+
}, function(err) {
|
|
917
|
+
el('optimize-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
918
|
+
show(el('optimize-results'));
|
|
919
|
+
indexAdvisor.disabled = false;
|
|
920
|
+
indexAdvisor.innerHTML = '⚡ Index Advisor';
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Server: Root Cause Analysis
|
|
926
|
+
var rootCauseBtn = el('server-root-cause');
|
|
927
|
+
if (rootCauseBtn) {
|
|
928
|
+
rootCauseBtn.addEventListener('click', function() {
|
|
929
|
+
rootCauseBtn.disabled = true;
|
|
930
|
+
rootCauseBtn.innerHTML = '<span class="mg-spinner"></span> Diagnosing...';
|
|
931
|
+
hide(el('server-ai-result'));
|
|
932
|
+
aiCall(ROUTES.root_cause, {}, function(data) {
|
|
933
|
+
el('server-ai-title').innerHTML = '<strong>⚡ Root Cause Analysis</strong>';
|
|
934
|
+
el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.');
|
|
935
|
+
show(el('server-ai-result'));
|
|
936
|
+
rootCauseBtn.disabled = false;
|
|
937
|
+
rootCauseBtn.innerHTML = '⚡ Why is it slow?';
|
|
938
|
+
}, function(err) {
|
|
939
|
+
el('server-ai-title').innerHTML = '<strong>⚡ Error</strong>';
|
|
940
|
+
el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
941
|
+
show(el('server-ai-result'));
|
|
942
|
+
rootCauseBtn.disabled = false;
|
|
943
|
+
rootCauseBtn.innerHTML = '⚡ Why is it slow?';
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
el('server-ai-close').addEventListener('click', function() { hide(el('server-ai-result')); });
|
|
949
|
+
|
|
950
|
+
// Server: Anomaly Detection
|
|
951
|
+
var anomalyBtn = el('server-anomaly');
|
|
952
|
+
if (anomalyBtn) {
|
|
953
|
+
anomalyBtn.addEventListener('click', function() {
|
|
954
|
+
anomalyBtn.disabled = true;
|
|
955
|
+
anomalyBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
956
|
+
hide(el('server-ai-result'));
|
|
957
|
+
aiCall(ROUTES.anomaly_detection, {}, function(data) {
|
|
958
|
+
el('server-ai-title').innerHTML = '<strong>⚡ Query Health Report</strong>';
|
|
959
|
+
el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.');
|
|
960
|
+
show(el('server-ai-result'));
|
|
961
|
+
anomalyBtn.disabled = false;
|
|
962
|
+
anomalyBtn.innerHTML = '⚡ Anomaly Detection';
|
|
963
|
+
}, function(err) {
|
|
964
|
+
el('server-ai-title').innerHTML = '<strong>⚡ Error</strong>';
|
|
965
|
+
el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
966
|
+
show(el('server-ai-result'));
|
|
967
|
+
anomalyBtn.disabled = false;
|
|
968
|
+
anomalyBtn.innerHTML = '⚡ Anomaly Detection';
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Schema Review
|
|
974
|
+
var schemaBtn = el('schema-review-btn');
|
|
975
|
+
if (schemaBtn) {
|
|
976
|
+
schemaBtn.addEventListener('click', function() {
|
|
977
|
+
schemaBtn.disabled = true;
|
|
978
|
+
schemaBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
|
|
979
|
+
hide(el('schema-result'));
|
|
980
|
+
var table = el('schema-table').value;
|
|
981
|
+
aiCall(ROUTES.schema_review, { table: table }, function(data) {
|
|
982
|
+
el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '');
|
|
983
|
+
show(el('schema-result'));
|
|
984
|
+
schemaBtn.disabled = false;
|
|
985
|
+
schemaBtn.innerHTML = '⚡ Analyze Schema';
|
|
986
|
+
}, function(err) {
|
|
987
|
+
el('schema-result-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
988
|
+
show(el('schema-result'));
|
|
989
|
+
schemaBtn.disabled = false;
|
|
990
|
+
schemaBtn.innerHTML = '⚡ Analyze Schema';
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Migration Risk
|
|
996
|
+
var migrationBtn = el('migration-assess-btn');
|
|
997
|
+
if (migrationBtn) {
|
|
998
|
+
migrationBtn.addEventListener('click', function() {
|
|
999
|
+
var migration = el('migration-input').value.trim();
|
|
1000
|
+
if (!migration) {
|
|
1001
|
+
el('migration-risk-badge').innerHTML = '';
|
|
1002
|
+
el('migration-result-content').innerHTML = '<div class="mg-alert mg-alert-warning">Please paste a Rails migration or DDL statement above.</div>';
|
|
1003
|
+
show(el('migration-result'));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
migrationBtn.disabled = true;
|
|
1007
|
+
migrationBtn.innerHTML = '<span class="mg-spinner"></span> Assessing...';
|
|
1008
|
+
hide(el('migration-result'));
|
|
1009
|
+
aiCall(ROUTES.migration_risk, { migration: migration }, function(data) {
|
|
1010
|
+
var level = (data.risk_level || '').toLowerCase();
|
|
1011
|
+
var badgeClass = level === 'critical' ? 'mg-badge-danger' : level === 'high' ? 'mg-badge-danger' : level === 'medium' ? 'mg-badge-warning' : 'mg-badge-info';
|
|
1012
|
+
el('migration-risk-badge').innerHTML = level ? '<span class="mg-badge ' + badgeClass + '" style="font-size:14px;padding:4px 12px;">Risk: ' + level.toUpperCase() + '</span>' : '';
|
|
1013
|
+
el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '');
|
|
1014
|
+
show(el('migration-result'));
|
|
1015
|
+
migrationBtn.disabled = false;
|
|
1016
|
+
migrationBtn.innerHTML = '⚡ Assess Risk';
|
|
1017
|
+
}, function(err) {
|
|
1018
|
+
el('migration-risk-badge').innerHTML = '';
|
|
1019
|
+
el('migration-result-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
|
|
1020
|
+
show(el('migration-result'));
|
|
1021
|
+
migrationBtn.disabled = false;
|
|
1022
|
+
migrationBtn.innerHTML = '⚡ Assess Risk';
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// --- SQL to Builder sync ---
|
|
1028
|
+
|
|
1029
|
+
function parseSqlToBuilder(sql) {
|
|
1030
|
+
if (!sql) return;
|
|
1031
|
+
if (sql.match(/\b(JOIN|GROUP\s+BY|HAVING|UNION)\b/i) || (sql.match(/SELECT/gi) || []).length > 1) return;
|
|
1032
|
+
var tableMatch = sql.match(/FROM\s+`?(\w+)`?/i);
|
|
1033
|
+
if (!tableMatch) return;
|
|
1034
|
+
var tableName = tableMatch[1];
|
|
1035
|
+
if (!qs('#vb-table option[value="' + tableName + '"]')) return;
|
|
1036
|
+
|
|
1037
|
+
var colMatch = sql.match(/SELECT\s+(.*?)\s+FROM/i);
|
|
1038
|
+
var selectedCols = [];
|
|
1039
|
+
if (colMatch && colMatch[1].trim() !== '*') {
|
|
1040
|
+
colMatch[1].split(',').forEach(function(p) { var n = p.trim().replace(/`/g, ''); if (n) selectedCols.push(n); });
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
var whereMatch = sql.match(/WHERE\s+(.*?)(?:\s+ORDER\s+BY\b|\s+LIMIT\b|\s*$)/i);
|
|
1044
|
+
var conditions = whereMatch ? parseWhereClause(whereMatch[1]) : [];
|
|
1045
|
+
|
|
1046
|
+
var orderMatch = sql.match(/ORDER\s+BY\s+(.*?)(?:\s+LIMIT\b|\s*$)/i);
|
|
1047
|
+
var orders = [];
|
|
1048
|
+
if (orderMatch) {
|
|
1049
|
+
orderMatch[1].split(',').forEach(function(p) {
|
|
1050
|
+
var m = p.trim().match(/`?(\w+)`?\s*(ASC|DESC)?/i);
|
|
1051
|
+
if (m) orders.push({ column: m[1], direction: (m[2] || 'ASC').toUpperCase() });
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
el('vb-table').value = tableName;
|
|
1056
|
+
loadColumnsForTable(tableName, function() {
|
|
1057
|
+
if (selectedCols.length) {
|
|
1058
|
+
qsa('.vb-col-check').forEach(function(c) { c.checked = selectedCols.indexOf(c.value) !== -1; });
|
|
1059
|
+
}
|
|
1060
|
+
el('vb-filters').innerHTML = '';
|
|
1061
|
+
conditions.forEach(function(cond) {
|
|
1062
|
+
addFilterRow(true);
|
|
1063
|
+
var row = el('vb-filters').lastElementChild;
|
|
1064
|
+
qs('.vb-filter-col', row).value = cond.column;
|
|
1065
|
+
var colType = columnTypeMap[cond.column] || 'string';
|
|
1066
|
+
var opSel = qs('.vb-filter-op', row);
|
|
1067
|
+
opSel.innerHTML = operatorsForType(colType).map(function(o) { return '<option>' + o + '</option>'; }).join('');
|
|
1068
|
+
opSel.value = cond.operator;
|
|
1069
|
+
updateValueInput(row, colType, cond.operator);
|
|
1070
|
+
if (cond.operator !== 'IS NULL' && cond.operator !== 'IS NOT NULL') {
|
|
1071
|
+
var valEl = qs('.vb-filter-val', row);
|
|
1072
|
+
if (valEl) valEl.value = cond.value;
|
|
1073
|
+
if (cond.operator === 'BETWEEN') { var endEl = qs('.vb-filter-val-end', row); if (endEl) endEl.value = cond.endValue; }
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
el('vb-orders').innerHTML = '';
|
|
1077
|
+
orders.forEach(function(ord) {
|
|
1078
|
+
addOrderRow(true);
|
|
1079
|
+
var row = el('vb-orders').lastElementChild;
|
|
1080
|
+
qs('.vb-order-col', row).value = ord.column;
|
|
1081
|
+
qs('.vb-order-dir', row).value = ord.direction;
|
|
1082
|
+
});
|
|
1083
|
+
updateGeneratedSql();
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function parseWhereClause(str) {
|
|
1088
|
+
var conditions = [];
|
|
1089
|
+
var betweenRe = /`?(\w+)`?\s+BETWEEN\s+'([^']*)'\s+AND\s+'([^']*)'/gi;
|
|
1090
|
+
var m;
|
|
1091
|
+
while ((m = betweenRe.exec(str)) !== null) conditions.push({ column: m[1], operator: 'BETWEEN', value: m[2], endValue: m[3] });
|
|
1092
|
+
var remaining = str.replace(betweenRe, '{{B}}');
|
|
1093
|
+
remaining.split(/\s+AND\s+/i).forEach(function(part) {
|
|
1094
|
+
part = part.trim();
|
|
1095
|
+
if (!part || part === '{{B}}') return;
|
|
1096
|
+
var m;
|
|
1097
|
+
if ((m = part.match(/`?(\w+)`?\s+IS\s+NOT\s+NULL/i))) { conditions.push({ column: m[1], operator: 'IS NOT NULL', value: '' }); return; }
|
|
1098
|
+
if ((m = part.match(/`?(\w+)`?\s+IS\s+NULL/i))) { conditions.push({ column: m[1], operator: 'IS NULL', value: '' }); return; }
|
|
1099
|
+
if ((m = part.match(/`?(\w+)`?\s+LIKE\s+'([^']*)'/i))) { conditions.push({ column: m[1], operator: 'LIKE', value: m[2] }); return; }
|
|
1100
|
+
if ((m = part.match(/`?(\w+)`?\s*(!=|>=|<=|=|>|<)\s*'([^']*)'/))) { conditions.push({ column: m[1], operator: m[2], value: m[3] }); return; }
|
|
1101
|
+
if ((m = part.match(/`?(\w+)`?\s*(!=|>=|<=|=|>|<)\s*(\d+\.?\d*)/))) { conditions.push({ column: m[1], operator: m[2], value: m[3] }); return; }
|
|
1102
|
+
});
|
|
1103
|
+
return conditions;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// --- Utilities ---
|
|
1107
|
+
|
|
1108
|
+
function setBtnLoading(ids, loading) {
|
|
1109
|
+
ids.forEach(function(id) {
|
|
1110
|
+
var btn = el(id);
|
|
1111
|
+
if (!btn) return;
|
|
1112
|
+
btn.disabled = loading;
|
|
1113
|
+
if (loading) { btn.dataset.origHtml = btn.innerHTML; btn.innerHTML = '<span class="mg-spinner"></span>'; }
|
|
1114
|
+
else if (btn.dataset.origHtml) { btn.innerHTML = btn.dataset.origHtml; }
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function formatMarkdown(text) {
|
|
1119
|
+
if (!text) return '';
|
|
1120
|
+
// Normalize literal \n from JSON strings to actual newlines
|
|
1121
|
+
text = text.replace(/\\n/g, '\n');
|
|
1122
|
+
return text
|
|
1123
|
+
.replace(/```sql\n?([\s\S]*?)```/g, '<pre class="mg-pre"><code>$1</code></pre>')
|
|
1124
|
+
.replace(/```\n?([\s\S]*?)```/g, '<pre class="mg-pre"><code>$1</code></pre>')
|
|
1125
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
1126
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
1127
|
+
.replace(/^####\s+(.+)$/gm, '<h5 style="margin:12px 0 4px;">$1</h5>')
|
|
1128
|
+
.replace(/^###\s+(.+)$/gm, '<h4 style="margin:16px 0 6px;">$1</h4>')
|
|
1129
|
+
.replace(/^##\s+(.+)$/gm, '<h3 style="margin:20px 0 8px;">$1</h3>')
|
|
1130
|
+
.replace(/^#\s+(.+)$/gm, '<h3 style="margin:20px 0 8px;">$1</h3>')
|
|
1131
|
+
.replace(/^---+$/gm, '<hr style="margin:12px 0;">')
|
|
1132
|
+
.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>')
|
|
1133
|
+
.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>')
|
|
1134
|
+
.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul style="margin:4px 0 4px 16px;padding:0;">$1</ul>')
|
|
1135
|
+
.replace(/<\/ul>\s*<ul[^>]*>/g, '')
|
|
1136
|
+
.replace(/\n\n/g, '<br><br>')
|
|
1137
|
+
.replace(/\n/g, '<br>');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function formatFindings(text) {
|
|
1141
|
+
if (!text) return '<div class="mg-text-muted">No findings.</div>';
|
|
1142
|
+
text = text.replace(/\\n/g, '\n');
|
|
1143
|
+
|
|
1144
|
+
// Split into severity sections
|
|
1145
|
+
var sections = [];
|
|
1146
|
+
var current = null;
|
|
1147
|
+
text.split('\n').forEach(function(line) {
|
|
1148
|
+
var heading = line.match(/^#{1,3}\s+(.+)/);
|
|
1149
|
+
if (heading) {
|
|
1150
|
+
var title = heading[1].replace(/\*\*/g, '');
|
|
1151
|
+
var severity = 'info';
|
|
1152
|
+
var titleLower = title.toLowerCase();
|
|
1153
|
+
if (titleLower.indexOf('critical') !== -1) severity = 'danger';
|
|
1154
|
+
else if (titleLower.indexOf('warning') !== -1) severity = 'warning';
|
|
1155
|
+
else if (titleLower.indexOf('suggestion') !== -1 || titleLower.indexOf('info') !== -1) severity = 'info';
|
|
1156
|
+
current = { title: title, severity: severity, lines: [] };
|
|
1157
|
+
sections.push(current);
|
|
1158
|
+
} else if (current) {
|
|
1159
|
+
current.lines.push(line);
|
|
1160
|
+
} else {
|
|
1161
|
+
if (!sections.length) sections.push({ title: '', severity: 'info', lines: [] });
|
|
1162
|
+
if (!current) current = sections[0];
|
|
1163
|
+
current.lines.push(line);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
if (!sections.length) return formatMarkdown(text);
|
|
1168
|
+
|
|
1169
|
+
var badgeColors = { danger: '#dc3545', warning: '#ffc107', info: '#17a2b8' };
|
|
1170
|
+
var bgColors = { danger: '#fff5f5', warning: '#fffbeb', info: '#f0f9ff' };
|
|
1171
|
+
var borderColors = { danger: '#f5c6cb', warning: '#ffeeba', info: '#bee5eb' };
|
|
1172
|
+
|
|
1173
|
+
return sections.map(function(sec) {
|
|
1174
|
+
var content = formatMarkdown(sec.lines.join('\n').trim());
|
|
1175
|
+
if (!content || content === '<br>') return '';
|
|
1176
|
+
var badge = badgeColors[sec.severity] || badgeColors.info;
|
|
1177
|
+
var bg = bgColors[sec.severity] || bgColors.info;
|
|
1178
|
+
var border = borderColors[sec.severity] || borderColors.info;
|
|
1179
|
+
return '<div class="mg-card mg-mb" style="border-left:4px solid ' + badge + ';background:' + bg + ';border-color:' + border + ';">' +
|
|
1180
|
+
(sec.title ? '<div class="mg-card-header" style="background:transparent;border-bottom:1px solid ' + border + ';"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
|
|
1181
|
+
'<div class="mg-card-body" style="font-size:13px;">' + content + '</div></div>';
|
|
1182
|
+
}).filter(function(s) { return s; }).join('');
|
|
1183
|
+
}
|
|
1184
|
+
})();
|
|
1185
|
+
</script>
|