mysql_genius-core 0.7.0 → 0.7.1

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.
@@ -0,0 +1,465 @@
1
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
2
+ <div style="display:flex;align-items:center;gap:12px;">
3
+ <a href="<%= path_for(:root) %>" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&larr; Dashboard</a>
4
+ <h4 style="margin:0;">Query Detail</h4>
5
+ </div>
6
+ <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';})()">
7
+ <script>document.write(document.documentElement.getAttribute('data-theme')==='dark'?'\u2600\uFE0F':'\uD83C\uDF19')</script>
8
+ </button>
9
+ </div>
10
+
11
+ <div id="qd-loading" class="mg-text-center"><span class="mg-spinner"></span> Loading query details...</div>
12
+ <div id="qd-error" class="mg-hidden"></div>
13
+
14
+ <div id="qd-content" class="mg-hidden">
15
+ <!-- SQL Block -->
16
+ <div class="mg-card mg-mb">
17
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
18
+ <strong>SQL</strong>
19
+ <div style="display:flex;gap:8px;">
20
+ <button id="qd-explain-btn" class="mg-btn mg-btn-outline mg-btn-sm">EXPLAIN</button>
21
+ <button id="qd-copy-btn" class="mg-btn mg-btn-outline-secondary mg-btn-sm">Copy</button>
22
+ </div>
23
+ </div>
24
+ <div class="mg-card-body">
25
+ <code class="mg-sql-block" id="qd-sql" style="max-height:none;"></code>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Explain Results -->
30
+ <div id="qd-explain-results" class="mg-card mg-mb mg-hidden">
31
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
32
+ <strong>EXPLAIN Output</strong>
33
+ <button id="qd-explain-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">Close</button>
34
+ </div>
35
+ <div class="mg-card-body">
36
+ <div id="qd-explain-loading" class="mg-text-center mg-hidden"><span class="mg-spinner"></span> Running EXPLAIN...</div>
37
+ <div class="mg-table-wrap">
38
+ <table class="mg-table" id="qd-explain-table" class="mg-hidden">
39
+ <thead id="qd-explain-thead"></thead>
40
+ <tbody id="qd-explain-tbody"></tbody>
41
+ </table>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Stats Summary Cards -->
47
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:16px;">
48
+ <div class="mg-card">
49
+ <div class="mg-card-body mg-text-center">
50
+ <div id="qd-stat-calls" style="font-size:20px;font-weight:700;">--</div>
51
+ <div class="mg-text-muted">Calls</div>
52
+ </div>
53
+ </div>
54
+ <div class="mg-card">
55
+ <div class="mg-card-body mg-text-center">
56
+ <div id="qd-stat-total-time" style="font-size:20px;font-weight:700;">--</div>
57
+ <div class="mg-text-muted">Total Time</div>
58
+ </div>
59
+ </div>
60
+ <div class="mg-card">
61
+ <div class="mg-card-body mg-text-center">
62
+ <div id="qd-stat-avg-time" style="font-size:20px;font-weight:700;">--</div>
63
+ <div class="mg-text-muted">Avg Time</div>
64
+ </div>
65
+ </div>
66
+ <div class="mg-card">
67
+ <div class="mg-card-body mg-text-center">
68
+ <div id="qd-stat-max-time" style="font-size:20px;font-weight:700;">--</div>
69
+ <div class="mg-text-muted">Max Time</div>
70
+ </div>
71
+ </div>
72
+ <div class="mg-card">
73
+ <div class="mg-card-body mg-text-center">
74
+ <div id="qd-stat-rows-examined" style="font-size:20px;font-weight:700;">--</div>
75
+ <div class="mg-text-muted">Rows Examined</div>
76
+ </div>
77
+ </div>
78
+ <div class="mg-card">
79
+ <div class="mg-card-body mg-text-center">
80
+ <div id="qd-stat-rows-sent" style="font-size:20px;font-weight:700;">--</div>
81
+ <div class="mg-text-muted">Rows Sent</div>
82
+ </div>
83
+ </div>
84
+ <div class="mg-card">
85
+ <div class="mg-card-body mg-text-center">
86
+ <div id="qd-stat-first-seen" style="font-size:14px;font-weight:700;">--</div>
87
+ <div class="mg-text-muted">First Seen</div>
88
+ </div>
89
+ </div>
90
+ <div class="mg-card">
91
+ <div class="mg-card-body mg-text-center">
92
+ <div id="qd-stat-last-seen" style="font-size:14px;font-weight:700;">--</div>
93
+ <div class="mg-text-muted">Last Seen</div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Charts -->
99
+ <div id="qd-charts-section">
100
+ <div id="qd-no-history" class="mg-text-center mg-text-muted mg-hidden" style="padding:24px 0;">
101
+ No history data available yet. The stats collector samples every 60 seconds.
102
+ </div>
103
+ <div id="qd-chart-total-time" class="mg-card mg-mb mg-hidden">
104
+ <div class="mg-card-header"><strong>Total Time (ms)</strong></div>
105
+ <div class="mg-card-body" id="qd-chart-total-time-container" style="height:200px;"></div>
106
+ </div>
107
+ <div id="qd-chart-avg-time" class="mg-card mg-mb mg-hidden">
108
+ <div class="mg-card-header"><strong>Average Time (ms)</strong></div>
109
+ <div class="mg-card-body" id="qd-chart-avg-time-container" style="height:200px;"></div>
110
+ </div>
111
+ <div id="qd-chart-calls" class="mg-card mg-mb mg-hidden">
112
+ <div class="mg-card-header"><strong>Calls</strong></div>
113
+ <div class="mg-card-body" id="qd-chart-calls-container" style="height:200px;"></div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <script>
119
+ (function() {
120
+ "use strict";
121
+
122
+ var DIGEST = '<%= @digest %>';
123
+ var EXPLAIN_URL = '<%= path_for(:explain) %>';
124
+ var HISTORY_URL = '<%= path_for(:query_history) %>';
125
+
126
+ var csrfMeta = document.querySelector('meta[name="csrf-token"]');
127
+ var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
128
+ var rawSql = '';
129
+
130
+ function el(id) { return document.getElementById(id); }
131
+ function show(e) { if (e) e.classList.remove('mg-hidden'); }
132
+ function hide(e) { if (e) e.classList.add('mg-hidden'); }
133
+ function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
134
+
135
+ function formatDuration(ms) {
136
+ if (ms >= 60000) return (ms / 60000).toFixed(1) + ' min';
137
+ if (ms >= 1000) return (ms / 1000).toFixed(1) + ' s';
138
+ return ms.toFixed(1) + ' ms';
139
+ }
140
+
141
+ function formatDurationStyled(ms) {
142
+ var text = formatDuration(ms);
143
+ var cls = 'mg-dur-fast';
144
+ if (ms >= 1000) cls = 'mg-dur-slow';
145
+ else if (ms >= 100) cls = 'mg-dur-moderate';
146
+ return '<span class="' + cls + '">' + text + '</span>';
147
+ }
148
+
149
+ // --- SQL Syntax Highlighting ---
150
+
151
+ var SQL_KW_SET = {};
152
+ '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; });
153
+
154
+ var SQL_FN_SET = {};
155
+ '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; });
156
+
157
+ var SQL_TOKEN_RE = /'[^']*'|`[^`]*`|\b\d+(?:\.\d+)?\b|[A-Za-z_]\w*|\?\|>=|<=|<>|!=|[><=]|[(),;*]|\s+|./g;
158
+
159
+ function highlightSql(sql) {
160
+ if (!sql) return '';
161
+ var result = [];
162
+ var match;
163
+ SQL_TOKEN_RE.lastIndex = 0;
164
+ while ((match = SQL_TOKEN_RE.exec(sql)) !== null) {
165
+ var tok = match[0];
166
+ var ch = tok.charAt(0);
167
+ if (ch === "'") {
168
+ result.push('<span class="mg-sql-str">' + escHtml(tok) + '</span>');
169
+ } else if (ch === '`') {
170
+ result.push('<span class="mg-sql-tbl">' + escHtml(tok) + '</span>');
171
+ } else if (/^\d/.test(tok)) {
172
+ result.push('<span class="mg-sql-num">' + escHtml(tok) + '</span>');
173
+ } else if (/^[A-Za-z_]/.test(tok)) {
174
+ var upper = tok.toUpperCase();
175
+ var rest = sql.substring(SQL_TOKEN_RE.lastIndex);
176
+ var compoundMatch = rest.match(/^(\s+)(BY)\b/i);
177
+ if ((upper === 'GROUP' || upper === 'ORDER' || upper === 'PARTITION') && compoundMatch) {
178
+ result.push('<span class="mg-sql-kw">' + escHtml(tok) + escHtml(compoundMatch[1]) + escHtml(compoundMatch[2].toUpperCase()) + '</span>');
179
+ SQL_TOKEN_RE.lastIndex += compoundMatch[0].length;
180
+ } else if (SQL_FN_SET[upper] && rest.match(/^\s*\(/)) {
181
+ result.push('<span class="mg-sql-fn">' + escHtml(tok.toUpperCase()) + '</span>');
182
+ } else if (SQL_KW_SET[upper]) {
183
+ result.push('<span class="mg-sql-kw">' + escHtml(tok.toUpperCase()) + '</span>');
184
+ } else {
185
+ result.push(escHtml(tok));
186
+ }
187
+ } else if (tok === '?') {
188
+ result.push('<span class="mg-sql-placeholder">?</span>');
189
+ } else if (tok === '*') {
190
+ result.push('<span class="mg-sql-star">*</span>');
191
+ } else if (/^(>=|<=|<>|!=|[><=])$/.test(tok)) {
192
+ result.push('<span class="mg-sql-op">' + escHtml(tok) + '</span>');
193
+ } else if (/^[(),;]$/.test(tok)) {
194
+ result.push('<span class="mg-sql-punc">' + escHtml(tok) + '</span>');
195
+ } else {
196
+ result.push(escHtml(tok));
197
+ }
198
+ }
199
+ return result.join('');
200
+ }
201
+
202
+ // --- AJAX ---
203
+
204
+ function ajax(method, url, data, onSuccess, onError) {
205
+ var xhr = new XMLHttpRequest();
206
+ xhr.open(method, url, true);
207
+ xhr.setRequestHeader('X-CSRF-Token', csrfToken);
208
+ xhr.setRequestHeader('Accept', 'application/json');
209
+ xhr.onload = function() {
210
+ var json;
211
+ try { json = JSON.parse(xhr.responseText); } catch(e) { json = null; }
212
+ if (xhr.status >= 200 && xhr.status < 300) {
213
+ onSuccess(json);
214
+ } else {
215
+ (onError || function(){})(json, xhr.status);
216
+ }
217
+ };
218
+ xhr.onerror = function() { (onError || function(){})(null, 0); };
219
+ if (data && method !== 'GET') {
220
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
221
+ var parts = [];
222
+ for (var k in data) {
223
+ parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
224
+ }
225
+ xhr.send(parts.join('&'));
226
+ } else {
227
+ xhr.send();
228
+ }
229
+ }
230
+
231
+ // --- SVG Chart Drawing ---
232
+
233
+ function drawChart(containerId, data, label, color) {
234
+ var container = el(containerId);
235
+ if (!container || !data || !data.length) return;
236
+
237
+ var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
238
+ var lineColor = isDark ? '#58a6ff' : '#89CFF0';
239
+ if (color) lineColor = isDark ? color.dark : color.light;
240
+ var textColor = isDark ? '#8b949e' : '#666';
241
+ var gridColor = isDark ? '#21262d' : '#eaecef';
242
+
243
+ var padding = { top: 10, right: 16, bottom: 30, left: 60 };
244
+ var width = 800;
245
+ var height = 200;
246
+ var chartW = width - padding.left - padding.right;
247
+ var chartH = height - padding.top - padding.bottom;
248
+
249
+ var values = data.map(function(d) { return typeof d.value === 'number' ? d.value : parseFloat(d.value) || 0; });
250
+ var maxVal = Math.max.apply(null, values);
251
+ // If all values are effectively zero, show a clean 0-1 scale
252
+ if (maxVal < 0.001) maxVal = 1;
253
+
254
+ // Y axis ticks (4-5 ticks)
255
+ var tickCount = 5;
256
+ var rawStep = maxVal / (tickCount - 1);
257
+ var magnitude = Math.pow(10, Math.floor(Math.log10(rawStep || 1)));
258
+ var niceStep = Math.ceil(rawStep / magnitude) * magnitude;
259
+ if (niceStep === 0) niceStep = 1;
260
+ var yMax = niceStep * (tickCount - 1);
261
+ if (yMax < maxVal) yMax = niceStep * tickCount;
262
+
263
+ var svg = '<svg viewBox="0 0 ' + width + ' ' + height + '" preserveAspectRatio="none" style="width:100%;height:100%;">';
264
+
265
+ // Grid lines and Y labels
266
+ for (var i = 0; i < tickCount; i++) {
267
+ var tickVal = niceStep * i;
268
+ var y = padding.top + chartH - (tickVal / yMax) * chartH;
269
+ svg += '<line x1="' + padding.left + '" y1="' + y + '" x2="' + (width - padding.right) + '" y2="' + y + '" stroke="' + gridColor + '" stroke-width="1"/>';
270
+ var tickLabel;
271
+ if (tickVal >= 1000000) tickLabel = (tickVal / 1000000).toFixed(1) + 'M';
272
+ else if (tickVal >= 1000) tickLabel = (tickVal / 1000).toFixed(1) + 'k';
273
+ else if (tickVal >= 1) tickLabel = Math.round(tickVal).toString();
274
+ else if (tickVal === 0) tickLabel = '0';
275
+ else tickLabel = tickVal.toFixed(2);
276
+ svg += '<text x="' + (padding.left - 8) + '" y="' + (y + 4) + '" text-anchor="end" fill="' + textColor + '" font-size="10" font-family="-apple-system,BlinkMacSystemFont,sans-serif">' + tickLabel + '</text>';
277
+ }
278
+
279
+ // Data points as polyline
280
+ var points = [];
281
+ for (var j = 0; j < data.length; j++) {
282
+ var x = padding.left + (j / Math.max(data.length - 1, 1)) * chartW;
283
+ var yVal = padding.top + chartH - (data[j].value / yMax) * chartH;
284
+ points.push(x.toFixed(1) + ',' + yVal.toFixed(1));
285
+ }
286
+
287
+ // Fill area below line
288
+ var fillPoints = points.slice();
289
+ fillPoints.push((padding.left + chartW).toFixed(1) + ',' + (padding.top + chartH).toFixed(1));
290
+ fillPoints.push(padding.left.toFixed(1) + ',' + (padding.top + chartH).toFixed(1));
291
+ svg += '<polygon points="' + fillPoints.join(' ') + '" fill="' + lineColor + '" fill-opacity="0.15"/>';
292
+
293
+ // Line
294
+ svg += '<polyline points="' + points.join(' ') + '" fill="none" stroke="' + lineColor + '" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>';
295
+
296
+ // X axis time labels (every ~4 hours worth of samples)
297
+ if (data.length > 1) {
298
+ var totalSpan = data.length - 1;
299
+ // Show ~6 labels max
300
+ var labelInterval = Math.max(1, Math.floor(totalSpan / 6));
301
+ for (var k = 0; k <= totalSpan; k += labelInterval) {
302
+ var xPos = padding.left + (k / totalSpan) * chartW;
303
+ var ts = data[k].timestamp;
304
+ var timeLabel = formatTimeLabel(ts);
305
+ svg += '<text x="' + xPos.toFixed(1) + '" y="' + (height - 6) + '" text-anchor="middle" fill="' + textColor + '" font-size="10" font-family="-apple-system,BlinkMacSystemFont,sans-serif">' + escHtml(timeLabel) + '</text>';
306
+ }
307
+ // Always show last label
308
+ if (totalSpan % labelInterval !== 0) {
309
+ var lastX = padding.left + chartW;
310
+ var lastTs = data[data.length - 1].timestamp;
311
+ svg += '<text x="' + lastX.toFixed(1) + '" y="' + (height - 6) + '" text-anchor="end" fill="' + textColor + '" font-size="10" font-family="-apple-system,BlinkMacSystemFont,sans-serif">' + escHtml(formatTimeLabel(lastTs)) + '</text>';
312
+ }
313
+ }
314
+
315
+ svg += '</svg>';
316
+ container.innerHTML = svg;
317
+ }
318
+
319
+ function formatTimeLabel(ts) {
320
+ if (!ts) return '';
321
+ var d = new Date(ts);
322
+ if (isNaN(d.getTime())) return ts;
323
+ var h = d.getHours();
324
+ var m = d.getMinutes();
325
+ return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
326
+ }
327
+
328
+ // --- Copy to Clipboard ---
329
+
330
+ function copyToClipboard(text, btn) {
331
+ var done = function() {
332
+ btn.textContent = 'Copied!';
333
+ setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
334
+ };
335
+ if (navigator.clipboard && navigator.clipboard.writeText) {
336
+ navigator.clipboard.writeText(text).then(done);
337
+ } else {
338
+ var ta = document.createElement('textarea');
339
+ ta.value = text;
340
+ ta.style.position = 'fixed';
341
+ ta.style.opacity = '0';
342
+ document.body.appendChild(ta);
343
+ ta.select();
344
+ document.execCommand('copy');
345
+ document.body.removeChild(ta);
346
+ done();
347
+ }
348
+ }
349
+
350
+ // --- Load Data ---
351
+
352
+ function loadQueryDetail() {
353
+ show(el('qd-loading'));
354
+ hide(el('qd-content'));
355
+ hide(el('qd-error'));
356
+
357
+ ajax('GET', HISTORY_URL, null, function(data) {
358
+ hide(el('qd-loading'));
359
+
360
+ if (!data || data.error) {
361
+ el('qd-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((data && data.error) || 'Query not found.') + '</div>';
362
+ show(el('qd-error'));
363
+ return;
364
+ }
365
+
366
+ show(el('qd-content'));
367
+
368
+ var q = data.query || {};
369
+ rawSql = q.sql || '';
370
+
371
+ // SQL block
372
+ el('qd-sql').innerHTML = highlightSql(rawSql);
373
+
374
+ // Stats cards
375
+ el('qd-stat-calls').textContent = Number(q.calls || 0).toLocaleString();
376
+ el('qd-stat-total-time').innerHTML = formatDurationStyled(q.total_time_ms || 0);
377
+ el('qd-stat-avg-time').innerHTML = formatDurationStyled(q.avg_time_ms || 0);
378
+ el('qd-stat-max-time').innerHTML = formatDurationStyled(q.max_time_ms || 0);
379
+ el('qd-stat-rows-examined').textContent = Number(q.rows_examined || 0).toLocaleString();
380
+ el('qd-stat-rows-sent').textContent = Number(q.rows_sent || 0).toLocaleString();
381
+ el('qd-stat-first-seen').textContent = q.first_seen || '--';
382
+ el('qd-stat-last-seen').textContent = q.last_seen || '--';
383
+
384
+ // Charts
385
+ var history = data.history || [];
386
+ if (history.length === 0) {
387
+ show(el('qd-no-history'));
388
+ } else {
389
+ hide(el('qd-no-history'));
390
+
391
+ var totalTimeData = history.map(function(h) { return { timestamp: h.timestamp, value: h.total_time_ms || 0 }; });
392
+ var avgTimeData = history.map(function(h) { return { timestamp: h.timestamp, value: h.avg_time_ms || 0 }; });
393
+ var callsData = history.map(function(h) { return { timestamp: h.timestamp, value: h.calls || 0 }; });
394
+
395
+ show(el('qd-chart-total-time'));
396
+ drawChart('qd-chart-total-time-container', totalTimeData, 'Total Time (ms)', { light: '#89CFF0', dark: '#58a6ff' });
397
+
398
+ show(el('qd-chart-avg-time'));
399
+ drawChart('qd-chart-avg-time-container', avgTimeData, 'Average Time (ms)', { light: '#89CFF0', dark: '#58a6ff' });
400
+
401
+ show(el('qd-chart-calls'));
402
+ drawChart('qd-chart-calls-container', callsData, 'Calls', { light: '#89CFF0', dark: '#58a6ff' });
403
+ }
404
+ }, function(json) {
405
+ hide(el('qd-loading'));
406
+ el('qd-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load query details.') + '</div>';
407
+ show(el('qd-error'));
408
+ });
409
+ }
410
+
411
+ // --- Explain ---
412
+
413
+ // performance_schema DIGEST_TEXT uses normalized spacing that isn't valid SQL:
414
+ // "SELECT COUNT ( * ) FROM `riders`" → needs to become "SELECT COUNT(*) FROM `riders`"
415
+ // "... IN ( ... )" → "... IN (...)"
416
+ // Also replaces placeholder ? with 1 so EXPLAIN can parse it.
417
+ function normalizeDigestSql(sql) {
418
+ return sql
419
+ .replace(/\(\s+/g, '(') // "( " → "("
420
+ .replace(/\s+\)/g, ')') // " )" → ")"
421
+ .replace(/\s*,\s*/g, ', ') // normalize comma spacing
422
+ .replace(/\?/g, '1'); // replace ? placeholders with literal 1
423
+ }
424
+
425
+ el('qd-explain-btn').addEventListener('click', function() {
426
+ show(el('qd-explain-results'));
427
+ show(el('qd-explain-loading'));
428
+ el('qd-explain-thead').innerHTML = '';
429
+ el('qd-explain-tbody').innerHTML = '';
430
+
431
+ ajax('POST', EXPLAIN_URL, { sql: normalizeDigestSql(rawSql) }, function(data) {
432
+ hide(el('qd-explain-loading'));
433
+ if (data.error) {
434
+ el('qd-explain-tbody').innerHTML = '<tr><td class="mg-text-muted">' + escHtml(data.error) + '</td></tr>';
435
+ return;
436
+ }
437
+ if (data.columns && data.rows) {
438
+ el('qd-explain-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
439
+ el('qd-explain-tbody').innerHTML = data.rows.map(function(row) {
440
+ return '<tr>' + row.map(function(cell) {
441
+ return '<td>' + (cell === null ? '<em class="null">NULL</em>' : escHtml(String(cell))) + '</td>';
442
+ }).join('') + '</tr>';
443
+ }).join('');
444
+ }
445
+ }, function(json) {
446
+ hide(el('qd-explain-loading'));
447
+ el('qd-explain-tbody').innerHTML = '<tr><td class="mg-text-muted">' + escHtml((json && json.error) || 'EXPLAIN failed.') + '</td></tr>';
448
+ });
449
+ });
450
+
451
+ el('qd-explain-close').addEventListener('click', function() {
452
+ hide(el('qd-explain-results'));
453
+ });
454
+
455
+ // --- Copy ---
456
+
457
+ el('qd-copy-btn').addEventListener('click', function() {
458
+ copyToClipboard(rawSql, el('qd-copy-btn'));
459
+ });
460
+
461
+ // --- Init ---
462
+
463
+ loadQueryDetail();
464
+ })();
465
+ </script>
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/gems/mysql_genius-core/CHANGELOG.md"
24
24
 
25
25
  spec.files = Dir.chdir(__dir__) do
26
- Dir.glob("lib/**/*.rb") + ["mysql_genius-core.gemspec", "CHANGELOG.md", "README.md"].select { |f| File.exist?(File.join(__dir__, f)) }
26
+ Dir.glob("lib/**/*.{rb,erb}") + ["mysql_genius-core.gemspec", "CHANGELOG.md", "README.md"].select { |f| File.exist?(File.join(__dir__, f)) }
27
27
  end
28
28
  spec.require_paths = ["lib"]
29
29
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
@@ -52,6 +52,18 @@ files:
52
52
  - lib/mysql_genius/core/server_info.rb
53
53
  - lib/mysql_genius/core/sql_validator.rb
54
54
  - lib/mysql_genius/core/version.rb
55
+ - lib/mysql_genius/core/views/mysql_genius/queries/_shared_results.html.erb
56
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_ai_tools.html.erb
57
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_dashboard.html.erb
58
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb
59
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_explorer.html.erb
60
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_stats.html.erb
61
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_server.html.erb
62
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_slow_queries.html.erb
63
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_table_sizes.html.erb
64
+ - lib/mysql_genius/core/views/mysql_genius/queries/_tab_unused_indexes.html.erb
65
+ - lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb
66
+ - lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb
55
67
  - mysql_genius-core.gemspec
56
68
  homepage: https://github.com/antarr/mysql_genius
57
69
  licenses: