Better looking settings tabs in pmahomme
[phpmyadmin.git] / js / server_status_monitor.js
blob12a41d398869c14f536616a198a801df0bf386a1
1 $(function() {
2     // Show tab links
3     $('div#statustabs_charting div.tabLinks').show();
4     $('div#statustabs_charting img#loadingMonitorIcon').remove();
5     // Codemirror is loaded on demand so we might need to initialize it
6     if (! codemirror_editor) {
7         var elm = $('#sqlquery');
8         if (elm.length > 0 && typeof CodeMirror != 'undefined') {
9             codemirror_editor = CodeMirror.fromTextArea(elm[0], { lineNumbers: true, matchBrackets: true, indentUnit: 4, mode: "text/x-mysql" });
10         }
11     }
12     // Timepicker is loaded on demand so we need to initialize datetime fields from the 'load log' dialog
13     $('div#logAnalyseDialog .datetimefield').each(function() {
14         PMA_addDatepicker($(this));
15     });
16     
17     /**** Monitor charting implementation ****/
18     /* Saves the previous ajax response for differential values */
19     var oldChartData = null;
20     // Holds about to created chart
21     var newChart = null;
22     var chartSpacing;
23     
24     // Whenever the monitor object (runtime.charts) or the settings object (monitorSettings) 
25     // changes in a way incompatible to the previous version, increase this number
26     // It will reset the users monitor and settings object in his localStorage to the default configuration
27     var monitorProtocolVersion = '1.0';
29     // Runtime parameter of the monitor, is being fully set in initGrid()
30     var runtime = {
31         // Holds all visible charts in the grid
32         charts: null,
33         // Stores the timeout handler so it can be cleared
34         refreshTimeout: null,
35         // Stores the GET request to refresh the charts
36         refreshRequest: null,
37         // Chart auto increment
38         chartAI: 0,
39         // To play/pause the monitor
40         redrawCharts: false,
41         // Object that contains a list of nodes that need to be retrieved from the server for chart updates
42         dataList: [],
43         // Current max points per chart (needed for auto calculation)
44         gridMaxPoints: 20,
45         // displayed time frame
46         xmin: -1,
47         xmax: -1
48     };
49     
50     var monitorSettings = null;
52     var defaultMonitorSettings = {
53         columns: 3,
54         chartSize: { width: 295, height: 250 },
55         // Max points in each chart. Settings it to 'auto' sets gridMaxPoints to (chartwidth - 40) / 12
56         gridMaxPoints: 'auto',
57         /* Refresh rate of all grid charts in ms */
58         gridRefresh: 5000
59     };
61     // Allows drag and drop rearrange and print/edit icons on charts
62     var editMode = false;
63     
64     /* List of preconfigured charts that the user may select */
65     var presetCharts = {
66         // Query cache efficiency
67         'qce': {
68             title: PMA_messages['strQueryCacheEfficiency'],
69             nodes: [ {
70                 name: PMA_messages['strQueryCacheEfficiency'],
71                 dataPoints: [{type: 'statusvar', name: 'Qcache_hits'}, {type: 'statusvar', name: 'Com_select'}],
72                 unit: '%',
73                 transformFn: 'qce'
74             } ]
75         },
76         // Query cache usage
77         'qcu': {
78             title: PMA_messages['strQueryCacheUsage'],
79             nodes: [ {
80                 name: PMA_messages['strQueryCacheUsed'],
81                 dataPoints: [{type: 'statusvar', name: 'Qcache_free_memory'}, {type: 'servervar', name: 'query_cache_size'}],
82                 unit: '%',
83                 transformFn: 'qcu'
84             } ]
85         }
86     };
87     
88     /* Add OS specific system info charts to the preset chart list */
89     switch(server_os) {
90     case 'WINNT': 
91         $.extend(presetCharts, { 
92             'cpu': {
93                 title: PMA_messages['strSystemCPUUsage'],
94                 nodes: [ { 
95                     name: PMA_messages['strAverageLoad'], 
96                     dataPoints: [{ type: 'cpu', name: 'loadavg'}], 
97                     unit: '%'
98                 } ]
99             },
100             
101             'memory': {
102                 title: PMA_messages['strSystemMemory'],
103                 nodes: [ {
104                     name: PMA_messages['strTotalMemory'],
105                     dataPoints: [{ type: 'memory', name: 'MemTotal' }],
106                     valueDivisor: 1024,
107                     unit: PMA_messages['strMiB']
108                 }, {
109                     dataType: 'memory',
110                       name: PMA_messages['strUsedMemory'],
111                       dataPoints: [{ type: 'memory', name: 'MemUsed' }],
112                       valueDivisor: 1024,
113                       unit: PMA_messages['strMiB']
114                 } ]
115             },
116             
117             'swap': {
118                 title: PMA_messages['strSystemSwap'],
119                 nodes: [ {
120                     name: PMA_messages['strTotalSwap'],
121                     dataPoints: [{ type: 'memory', name: 'SwapTotal' }],
122                     valueDivisor: 1024,
123                     unit: PMA_messages['strMiB']
124                 }, {
125                     name: PMA_messages['strUsedSwap'],
126                     dataPoints: [{ type: 'memory', name: 'SwapUsed' }],
127                     valueDivisor: 1024,
128                     unit: PMA_messages['strMiB']
129                 } ]
130             }
131         });
132         break;
133         
134     case 'Linux':
135         $.extend(presetCharts, {
136             'cpu': {
137                 title: PMA_messages['strSystemCPUUsage'],
138                 nodes: [ {
139                     name: PMA_messages['strAverageLoad'],
140                     dataPoints: [{ type: 'cpu', name: 'irrelevant' }],
141                     unit: '%',
142                     transformFn: 'cpu-linux'
143                 } ]
144             },
145             'memory': {
146                 title: PMA_messages['strSystemMemory'],
147                 nodes: [
148                     { name: PMA_messages['strUsedMemory'], dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024, unit: PMA_messages['strMiB'] },
149                     { name: PMA_messages['strCachedMemory'], dataPoints: [{ type: 'memory', name: 'Cached' }],  valueDivisor: 1024, unit: PMA_messages['strMiB'] },
150                     { name: PMA_messages['strBufferedMemory'], dataPoints: [{ type: 'memory', name: 'Buffers' }], valueDivisor: 1024, unit: PMA_messages['strMiB'] },
151                     { name: PMA_messages['strFreeMemory'], dataPoints: [{ type: 'memory', name: 'MemFree' }], valueDivisor: 1024, unit: PMA_messages['strMiB'] }
152                 ],
153                 settings: {
154                     chart: {
155                         type: 'area',
156                         animation: false
157                     },
158                     plotOptions: {
159                         area: {
160                             stacking: 'percent'
161                         }
162                     }
163                 }
164             },
165             'swap': {
166                 title: PMA_messages['strSystemSwap'],
167                 nodes: [
168                     { name: PMA_messages['strUsedSwap'], dataPoints: [{ type: 'memory', name: 'SwapUsed' }], valueDivisor: 1024, unit: PMA_messages['strMiB'] },
169                     { name: PMA_messages['strCachedSwap'], dataPoints: [{ type: 'memory', name: 'SwapCached' }], valueDivisor: 1024, unit: PMA_messages['strMiB'] },
170                     { name: PMA_messages['strFreeSwap'], dataPoints: [{ type: 'memory', name: 'SwapFree' }], valueDivisor: 1024, unit: PMA_messages['strMiB'] }
171                 ],
172                 settings: {
173                     chart: {
174                         type: 'area',
175                         animation: false
176                     },
177                     plotOptions: {
178                         area: {
179                             stacking: 'percent'
180                         }
181                     }
182                 }
183             }
184         });
185         break;
186     }
188     // Default setting for the chart grid
189     defaultChartGrid = {
190         'c0': {  title: PMA_messages['strQuestions'],
191                  nodes: [{name: PMA_messages['strQuestions'], dataPoints: [{ type: 'statusvar', name: 'Questions' }], display: 'differential' }]
192         },
193         'c1': {
194                  title: PMA_messages['strChartConnectionsTitle'],
195                  nodes: [ { name: PMA_messages['strConnections'], dataPoints: [{ type: 'statusvar', name: 'Connections' }], display: 'differential' },
196                           { name: PMA_messages['strProcesses'], dataPoints: [{ type: 'proc', name: 'processes' }] } ]
197         },
198         'c2': {
199                  title: PMA_messages['strTraffic'],
200                  nodes: [
201                     { name: PMA_messages['strBytesSent'], dataPoints: [{ type: 'statusvar', name: 'Bytes_sent' }], display: 'differential', valueDivisor: 1024, unit: PMA_messages['strKiB'] },
202                     { name: PMA_messages['strBytesReceived'], dataPoints: [{ type: 'statusvar', name: 'Bytes_received' }], display: 'differential', valueDivisor: 1024, unit: PMA_messages['strKiB'] }
203                  ]
204          }
205     };
207     // Server is localhost => We can add cpu/memory/swap to the default chart
208     if (server_db_isLocal) {
209         defaultChartGrid['c3'] = presetCharts['cpu'];
210         defaultChartGrid['c4'] = presetCharts['memory'];
211         defaultChartGrid['c5'] = presetCharts['swap'];
212     }
214     /* Buttons that are on the top right corner of each chart */
215     var gridbuttons = {
216         cogButton: {
217             //enabled: true,
218             symbol: 'url(' + pmaThemeImage  + 's_cog.png)',
219             x: -36,
220             symbolFill: '#B5C9DF',
221             hoverSymbolFill: '#779ABF',
222             _titleKey: 'settings',
223             menuName: 'gridsettings',
224             menuItems: [{
225                 textKey: 'editChart',
226                 onclick: function() {
227                     editChart(this);
228                 }
229             }, {
230                 textKey: 'removeChart',
231                 onclick: function() {
232                     removeChart(this);
233                 }
234             }]
235         }
236     };
238     Highcharts.setOptions({
239         lang: {
240             settings:    PMA_messages['strSettings'],
241             removeChart: PMA_messages['strRemoveChart'],
242             editChart:   PMA_messages['strEditChart']
243         }
244     });
246     $('a[href="#rearrangeCharts"], a[href="#endChartEditMode"]').click(function() {
247         editMode = !editMode;
248         if ($(this).attr('href') == '#endChartEditMode') {
249             editMode = false;
250         }
252         // Icon graphics have zIndex 19, 20 and 21. Let's just hope nothing else has the same zIndex
253         $('table#chartGrid div svg').find('*[zIndex=20], *[zIndex=21], *[zIndex=19]').toggle(editMode)
255         $('a[href="#endChartEditMode"]').toggle(editMode);
257         if (editMode) {
258             // Close the settings popup
259             $('#statustabs_charting .popupContent').hide().removeClass('openedPopup');
261             $("#chartGrid").sortableTable({
262                 ignoreRect: {
263                     top: 8,
264                     left: chartSize().width - 63,
265                     width: 54,
266                     height: 24
267                 },
268                 events: {
269                     // Drop event. The drag child element is moved into the drop element
270                     // and vice versa. So the parameters are switched.
271                     drop: function(drag, drop, pos) {
272                         var dragKey, dropKey, dropRender;
273                         var dragRender = $(drag).children().first().attr('id');
275                         if ($(drop).children().length > 0) {
276                             dropRender = $(drop).children().first().attr('id');
277                         }
279                         // Find the charts in the array
280                         $.each(runtime.charts, function(key, value) {
281                             if (value.chart.options.chart.renderTo == dragRender) {
282                                 dragKey = key;
283                             }
284                             if (dropRender && value.chart.options.chart.renderTo == dropRender) {
285                                 dropKey = key;
286                             }
287                         });
289                         // Case 1: drag and drop are charts -> Switch keys
290                         if (dropKey) {
291                             if (dragKey) {
292                                 dragChart = runtime.charts[dragKey];
293                                 runtime.charts[dragKey] = runtime.charts[dropKey];
294                                 runtime.charts[dropKey] = dragChart;
295                             } else {
296                                 // Case 2: drop is a empty cell => just completely rebuild the ids
297                                 var keys = [];
298                                 var dropKeyNum = parseInt(dropKey.substr(1));
299                                 var insertBefore = pos.col + pos.row * monitorSettings.columns;
300                                 var values = [];
301                                 var newChartList = {};
302                                 var c = 0;
304                                 $.each(runtime.charts, function(key, value) {
305                                     if (key != dropKey) {
306                                         keys.push(key);
307                                     }
308                                 });
310                                 keys.sort();
312                                 // Rebuilds all ids, with the dragged chart correctly inserted
313                                 for (var i = 0; i<keys.length; i++) {
314                                     if (keys[i] == insertBefore) {
315                                         newChartList['c' + (c++)] = runtime.charts[dropKey];
316                                         insertBefore = -1; // Insert ok
317                                     }
318                                     newChartList['c' + (c++)] = runtime.charts[keys[i]];
319                                 }
321                                 // Not inserted => put at the end
322                                 if (insertBefore != -1) {
323                                     newChartList['c' + (c++)] = runtime.charts[dropKey];
324                                 }
326                                 runtime.charts = newChartList;
327                             }
329                             saveMonitor();
330                         }
331                     }
332                 }
333             });
335         } else {
336             $("#chartGrid").sortableTable('destroy');
337             saveMonitor(); // Save settings
338         }
340         return false;
341     });
343     // global settings
344     $('div#statustabs_charting div.popupContent select[name="chartColumns"]').change(function() {
345         monitorSettings.columns = parseInt(this.value);
347         var newSize = chartSize();
349         // Empty cells should keep their size so you can drop onto them
350         $('table#chartGrid tr td').css('width', newSize.width + 'px');
352         /* Reorder all charts that it fills all column cells */
353         var numColumns;
354         var $tr = $('table#chartGrid tr:first');
355         var row = 0;
356         while($tr.length != 0) {
357             numColumns = 1;
358             // To many cells in one row => put into next row
359             $tr.find('td').each(function() {
360                 if (numColumns > monitorSettings.columns) {
361                     if ($tr.next().length == 0) { 
362                         $tr.after('<tr></tr>');
363                     }
364                     $tr.next().prepend($(this));
365                 }
366                 numColumns++;
367             });
369             // To little cells in one row => for each cell to little, move all cells backwards by 1
370             if ($tr.next().length > 0) {
371                 var cnt = monitorSettings.columns - $tr.find('td').length;
372                 for (var i = 0; i < cnt; i++) {
373                     $tr.append($tr.next().find('td:first'));
374                     $tr.nextAll().each(function() {
375                         if ($(this).next().length != 0) {
376                             $(this).append($(this).next().find('td:first'));
377                         }
378                     });
379                 }
380             }
382             $tr = $tr.next();
383             row++;
384         }
386         /* Apply new chart size to all charts */
387         $.each(runtime.charts, function(key, value) {
388             value.chart.setSize(
389                 newSize.width,
390                 newSize.height,
391                 false
392             );
393         });
395         if (monitorSettings.gridMaxPoints == 'auto') {
396             runtime.gridMaxPoints = Math.round((newSize.width - 40) / 12);
397         }
399         runtime.xmin = new Date().getTime() - server_time_diff - runtime.gridMaxPoints * monitorSettings.gridRefresh;
400         runtime.xmax = new Date().getTime() - server_time_diff + monitorSettings.gridRefresh;
402         if (editMode) {
403             $("#chartGrid").sortableTable('refresh');
404         }
406         saveMonitor(); // Save settings
407     });
409     $('div#statustabs_charting div.popupContent select[name="gridChartRefresh"]').change(function() {
410         monitorSettings.gridRefresh = parseInt(this.value) * 1000;
411         clearTimeout(runtime.refreshTimeout);
413         if (runtime.refreshRequest) {
414             runtime.refreshRequest.abort();
415         }
417         runtime.xmin = new Date().getTime() - server_time_diff - runtime.gridMaxPoints * monitorSettings.gridRefresh;
418         runtime.xmax = new Date().getTime() - server_time_diff + monitorSettings.gridRefresh;
420         $.each(runtime.charts, function(key, value) {
421             value.chart.xAxis[0].setExtremes(runtime.xmin, runtime.xmax, false);
422         });
424         runtime.refreshTimeout = setTimeout(refreshChartGrid, monitorSettings.gridRefresh);
426         saveMonitor(); // Save settings
427     });
429     $('a[href="#addNewChart"]').click(function() {
430         var dlgButtons = { };
432         dlgButtons[PMA_messages['strAddChart']] = function() {
433             var type = $('input[name="chartType"]:checked').val();
435             if (type == 'preset') {
436                 newChart = presetCharts[$('div#addChartDialog select[name="presetCharts"]').prop('value')];
437             } else {
438                 // If user builds his own chart, it's being set/updated each time he adds a series
439                 // So here we only warn if he didn't add a series yet
440                 if (! newChart || ! newChart.nodes || newChart.nodes.length == 0) {
441                     alert(PMA_messages['strAddOneSeriesWarning']);
442                     return;
443                 }
444             }
446             newChart.title = $('input[name="chartTitle"]').attr('value');
447             // Add a cloned object to the chart grid
448             addChart($.extend(true, {}, newChart));
450             newChart = null;
452             saveMonitor(); // Save settings
454             $(this).dialog("close");
455         };
456         
457         dlgButtons[PMA_messages['strClose']] = function() {
458             newChart = null;
459             $('span#clearSeriesLink').hide();
460             $('#seriesPreview').html('');
461             $(this).dialog("close");
462         };
463         
464         var $presetList = $('div#addChartDialog select[name="presetCharts"]');
465         if ($presetList.html().length == 0) {
466             $.each(presetCharts, function(key, value) {
467                 $presetList.append('<option value="' + key + '">' + value.title + '</option>');
468             });
469             $presetList.change(function() {
470                 $('input#chartPreset').trigger('click');
471                 $('input[name="chartTitle"]').attr('value', presetCharts[$(this).prop('value')].title);
472             });
473         }
474         
475         $('div#addChartDialog').dialog({
476             width: 'auto',
477             height: 'auto',
478             buttons: dlgButtons
479         });
481         $('div#addChartDialog #seriesPreview').html('<i>' + PMA_messages['strNone'] + '</i>');
483         return false;
484     });
485     
486     $('a[href="#exportMonitorConfig"]').click(function() {
487         var gridCopy = {};
489         $.each(runtime.charts, function(key, elem) {
490             gridCopy[key] = {};
491             gridCopy[key].nodes = elem.nodes;
492             gridCopy[key].settings = elem.settings;
493             gridCopy[key].title = elem.title;
494         });
495         
496         var exportData = {
497             monitorCharts: gridCopy,
498             monitorSettings: monitorSettings
499         };
500         var $form;
501         
502         $('body').append($form = $('<form method="post" action="file_echo.php?' + url_query + '&filename=1" style="display:none;"></form>'));
503         
504         $form.append('<input type="hidden" name="monitorconfig" value="' + encodeURI($.toJSON(exportData)) + '">');
505         $form.submit();
506         $form.remove();
507     });
509     $('a[href="#importMonitorConfig"]').click(function() {
510         $('div#emptyDialog').attr('title', 'Import monitor configuration');
511         $('div#emptyDialog').html('Please select the file you want to import:<br/><form action="file_echo.php?' + url_query + '&import=1" method="post" enctype="multipart/form-data">' +
512             '<input type="file" name="file"> <input type="hidden" name="import" value="1"> </form>');
513         
514         var dlgBtns = {};
515         
516         dlgBtns[PMA_messages['strImport']] = function() {
517             var $iframe, $form;
518             $('body').append($iframe = $('<iframe id="monitorConfigUpload" style="display:none;"></iframe>'));
519             var d = $iframe[0].contentWindow.document;
520             d.open(); d.close();
521             mew = d;
522             
523             $iframe.load(function() {
524                 var json;
526                 // Try loading config
527                 try {
528                     var data = $('body', $('iframe#monitorConfigUpload')[0].contentWindow.document).html();
529                     // Chrome wraps around '<pre style="word-wrap: break-word; white-space: pre-wrap;">' to any text content -.-
530                     json = $.secureEvalJSON(data.substring(data.indexOf("{"), data.lastIndexOf("}") + 1));
531                 } catch (err) {
532                     alert(PMA_messages['strFailedParsingConfig']);
533                     $('div#emptyDialog').dialog('close');
534                     return;
535                 }
536             
537                 // Basic check, is this a monitor config json?
538                 if (!json || ! json.monitorCharts || ! json.monitorCharts) {
539                     alert(PMA_messages['strFailedParsingConfig']);
540                     $('div#emptyDialog').dialog('close');
541                     return;
542                 }
543                 
544                 // If json ok, try applying config
545                 try {
546                     window.localStorage['monitorCharts'] = $.toJSON(json.monitorCharts);
547                     window.localStorage['monitorSettings'] = $.toJSON(json.monitorSettings);
548                     rebuildGrid();
549                 } catch(err) {
550                     alert(PMA_messages['strFailedBuildingGrid']);
551                     // If an exception is thrown, load default again
552                     window.localStorage.removeItem('monitorCharts');
553                     window.localStorage.removeItem('monitorSettings');
554                     rebuildGrid();
555                 }
556                 
557                 $('div#emptyDialog').dialog('close');
558             });
559             
560             $("body", d).append($form = $('div#emptyDialog').find('form'));
561             $form.submit();
562             $('div#emptyDialog').append('<img class="ajaxIcon" src="' + pmaThemeImage + 'ajax_clock_small.gif" alt="">');
563         };
564         
565         dlgBtns[PMA_messages['strCancel']] = function() {
566             $(this).dialog('close');
567         }
568         
569         
570         $('div#emptyDialog').dialog({
571             width: 'auto',
572             height: 'auto',
573             buttons: dlgBtns
574         });
575     });
577     $('a[href="#clearMonitorConfig"]').click(function() {
578         window.localStorage.removeItem('monitorCharts');
579         window.localStorage.removeItem('monitorSettings');
580         window.localStorage.removeItem('monitorVersion');
581         $(this).hide();
582         rebuildGrid();
583     });
585     $('a[href="#pauseCharts"]').click(function() {
586         runtime.redrawCharts = ! runtime.redrawCharts;
587         if (! runtime.redrawCharts) {
588             $(this).html('<img src="themes/dot.gif" class="icon ic_play" alt="" /> ' + PMA_messages['strResumeMonitor']);
589         } else {
590             $(this).html('<img src="themes/dot.gif" class="icon ic_pause" alt="" /> ' + PMA_messages['strPauseMonitor']);
591             if (! runtime.charts) {
592                 initGrid();
593                 $('a[href="#settingsPopup"]').show();
594             }
595         }
596         return false;
597     });
598     
599     $('a[href="#monitorInstructionsDialog"]').click(function() {
600         var $dialog = $('div#monitorInstructionsDialog');
602         $dialog.dialog({
603             width: 595,
604             height: 'auto'
605         }).find('img.ajaxIcon').show();
607         var loadLogVars = function(getvars) {
608             vars = { ajax_request: true, logging_vars: true };
609             if (getvars) {
610                 $.extend(vars, getvars);
611             }
613             $.get('server_status.php?' + url_query, vars,
614                 function(data) {
615                     var logVars = $.parseJSON(data),
616                         icon = 'ic_s_success', msg='', str='';
618                     if (logVars['general_log'] == 'ON') {
619                         if (logVars['slow_query_log'] == 'ON') {
620                             msg = PMA_messages['strBothLogOn'];
621                         } else {
622                             msg = PMA_messages['strGenLogOn'];
623                         }
624                     }
626                     if (msg.length == 0 && logVars['slow_query_log'] == 'ON') {
627                         msg = PMA_messages['strSlowLogOn'];
628                     }
630                     if (msg.length == 0) {
631                         icon = 'ic_s_error';
632                         msg = PMA_messages['strBothLogOff'];
633                     }
635                     str = '<b>' + PMA_messages['strCurrentSettings'] + '</b><br><div class="smallIndent">';
636                     str += '<img src="themes/dot.gif" class="icon ' + icon + '" alt=""/> ' + msg + '<br />';
638                     if (logVars['log_output'] != 'TABLE') {
639                         str += '<img src="themes/dot.gif" class="icon ic_s_error" alt=""/> ' + PMA_messages['strLogOutNotTable'] + '<br />';
640                     } else {
641                         str += '<img src="themes/dot.gif" class="icon ic_s_success" alt=""/> ' + PMA_messages['strLogOutIsTable'] + '<br />';
642                     }
644                     if (logVars['slow_query_log'] == 'ON') {
645                         if (logVars['long_query_time'] > 2) {
646                             str += '<img src="themes/dot.gif" class="icon ic_s_attention" alt=""/> '
647                                 + $.sprintf(PMA_messages['strSmallerLongQueryTimeAdvice'], logVars['long_query_time'])
648                                 + '<br />';
649                         }
650                         
651                         if (logVars['long_query_time'] < 2) {
652                             str += '<img src="themes/dot.gif" class="icon ic_s_success" alt=""/> '
653                                 + $.sprintf(PMA_messages['strLongQueryTimeSet'], logVars['long_query_time'])
654                                 + '<br />';
655                         }
656                     }
658                     str += '</div>';
660                     if (is_superuser) {
661                         str += '<p></p><b>Change settings</b>';
662                         str += '<div class="smallIndent">';
663                         str += PMA_messages['strSettingsAppliedGlobal'] + '<br/>';
665                         var varValue = 'TABLE';
666                         if (logVars['log_output'] == 'TABLE') {
667                             varValue = 'FILE';
668                         }
669                         
670                         str += '- <a class="set" href="#log_output-' + varValue + '">'
671                             + $.sprintf(PMA_messages['strSetLogOutput'], varValue)
672                             + ' </a><br />';
674                         if (logVars['general_log'] != 'ON') {
675                             str += '- <a class="set" href="#general_log-ON">'
676                                 + $.sprintf(PMA_messages['strEnableVar'], 'general_log')
677                                 + ' </a><br />';
678                         } else {
679                             str += '- <a class="set" href="#general_log-OFF">'
680                                 + $.sprintf(PMA_messages['strDisableVar'], 'general_log')
681                                 + ' </a><br />';
682                         }
684                         if (logVars['slow_query_log'] != 'ON') {
685                             str += '- <a class="set" href="#slow_query_log-ON">'
686                                 +  $.sprintf(PMA_messages['strEnableVar'], 'slow_query_log')
687                                 + ' </a><br />';
688                         } else {
689                             str += '- <a class="set" href="#slow_query_log-OFF">'
690                                 +  $.sprintf(PMA_messages['strDisableVar'], 'slow_query_log')
691                                 + ' </a><br />';
692                         }
694                         varValue = 5;
695                         if (logVars['long_query_time'] > 2) {
696                             varValue = 1;
697                         }
699                         str += '- <a class="set" href="#long_query_time-' + varValue + '">'
700                             + $.sprintf(PMA_messages['setSetLongQueryTime'], varValue)
701                             + ' </a><br />';
703                     } else {
704                         str += PMA_messages['strNoSuperUser'] + '<br/>';
705                     }
707                     str += '</div>';
709                     $dialog.find('div.monitorUse').toggle(
710                         logVars['log_output'] == 'TABLE' && (logVars['slow_query_log'] == 'ON' || logVars['general_log'] == 'ON')
711                     );
713                     $dialog.find('div.ajaxContent').html(str);
714                     $dialog.find('img.ajaxIcon').hide();
715                     $dialog.find('a.set').click(function() {
716                         var nameValue = $(this).attr('href').split('-');
717                         loadLogVars({ varName: nameValue[0].substr(1), varValue: nameValue[1]});
718                         $dialog.find('img.ajaxIcon').show();
719                     });
720                 }
721             );
722         };
723         
724         
725         loadLogVars();
727         return false;
728     });
730     $('input[name="chartType"]').change(function() {
731         $('#chartVariableSettings').toggle(this.checked && this.value == 'variable');
732         var title = $('input[name="chartTitle"]').attr('value');
733         if (title == PMA_messages['strChartTitle'] 
734            || title == $('label[for="' + $('input[name="chartTitle"]').data('lastRadio') + '"]').text()
735         ) {
736             $('input[name="chartTitle"]')
737                 .data('lastRadio', $(this).attr('id'))
738                 .attr('value', $('label[for="' + $(this).attr('id') + '"]').text());
739         }
741     });
743     $('input[name="useDivisor"]').change(function() {
744         $('span.divisorInput').toggle(this.checked);
745     });
746     $('input[name="useUnit"]').change(function() {
747         $('span.unitInput').toggle(this.checked);
748     });
750     $('select[name="varChartList"]').change(function () {
751         if (this.selectedIndex != 0) {
752             $('#variableInput').attr('value', this.value);
753         }
754     });
756     $('a[href="#kibDivisor"]').click(function() {
757         $('input[name="valueDivisor"]').attr('value', 1024);
758         $('input[name="valueUnit"]').attr('value', PMA_messages['strKiB']);
759         $('span.unitInput').toggle(true);
760         $('input[name="useUnit"]').prop('checked', true);
761         return false;
762     });
764     $('a[href="#mibDivisor"]').click(function() {
765         $('input[name="valueDivisor"]').attr('value', 1024*1024);
766         $('input[name="valueUnit"]').attr('value', PMA_messages['strMiB']);
767         $('span.unitInput').toggle(true);
768         $('input[name="useUnit"]').prop('checked', true);
769         return false;
770     });
772     $('a[href="#submitClearSeries"]').click(function() {
773         $('#seriesPreview').html('<i>' + PMA_messages['strNone'] + '</i>');
774         newChart = null;
775         $('span#clearSeriesLink').hide();
776     });
778     $('a[href="#submitAddSeries"]').click(function() {
779         if ($('input#variableInput').attr('value').length == 0) {
780             return false;
781         }
782         
783         if (newChart == null) {
784             $('#seriesPreview').html('');
786             newChart = {
787                 title: $('input[name="chartTitle"]').attr('value'),
788                 nodes: []
789             };
790         }
792         var serie = {
793             dataPoints: [{ type: 'statusvar', name: $('input#variableInput').attr('value') }],
794             name: $('input#variableInput').attr('value'),
795             display: $('input[name="differentialValue"]').attr('checked') ? 'differential' : ''
796         };
798         if (serie.dataPoint == 'Processes') {
799             serie.dataType='proc';
800         }
802         if ($('input[name="useDivisor"]').attr('checked')) {
803             serie.valueDivisor = parseInt($('input[name="valueDivisor"]').attr('value'));
804         }
806         if ($('input[name="useUnit"]').attr('checked')) {
807             serie.unit = $('input[name="valueUnit"]').attr('value');
808         }
810         var str = serie.display == 'differential' ? ', ' + PMA_messages['strDifferential'] : '';
811         str += serie.valueDivisor ? (', ' + $.sprintf(PMA_messages['strDividedBy'], serie.valueDivisor)) : '';
813         $('#seriesPreview').append('- ' + serie.name + str + '<br>');
815         newChart.nodes.push(serie);
817         $('input#variableInput').attr('value', '');
818         $('input[name="differentialValue"]').attr('checked', true);
819         $('input[name="useDivisor"]').attr('checked', false);
820         $('input[name="useUnit"]').attr('checked', false);
821         $('input[name="useDivisor"]').trigger('change');
822         $('input[name="useUnit"]').trigger('change');
823         $('select[name="varChartList"]').get(0).selectedIndex = 0;
825         $('span#clearSeriesLink').show();
827         return false;
828     });
830     $("input#variableInput").autocomplete({
831             source: variableNames
832     });
834     /* Initializes the monitor, called only once */
835     function initGrid() {
836         var settings;
837         var series;
839         /* Apply default values & config */
840         if (window.localStorage) {
841             if (window.localStorage['monitorCharts']) {
842                 runtime.charts = $.parseJSON(window.localStorage['monitorCharts']);
843             }
844             if (window.localStorage['monitorSettings']) {
845                 monitorSettings = $.parseJSON(window.localStorage['monitorSettings']);
846             }
848             $('a[href="#clearMonitorConfig"]').toggle(runtime.charts != null);
850             if (runtime.charts != null && monitorProtocolVersion != window.localStorage['monitorVersion']) {
851                 $('div#emptyDialog').attr('title',PMA_messages['strIncompatibleMonitorConfig']);
852                 $('div#emptyDialog').html(PMA_messages['strIncompatibleMonitorConfigDescription']);
854                 var dlgBtns = {};
855                 dlgBtns[PMA_messages['strClose']] = function() { $(this).dialog('close'); };
857                 $('div#emptyDialog').dialog({ 
858                     width: 400,
859                     buttons: dlgBtns 
860                 });
861             }            
862         }
864         if (runtime.charts == null) {
865             runtime.charts = defaultChartGrid;
866         }
867         if (monitorSettings == null) {
868             monitorSettings = defaultMonitorSettings;
869         }
871         $('select[name="gridChartRefresh"]').attr('value', monitorSettings.gridRefresh / 1000);
872         $('select[name="chartColumns"]').attr('value', monitorSettings.columns);
874         if (monitorSettings.gridMaxPoints == 'auto') {
875             runtime.gridMaxPoints = Math.round((monitorSettings.chartSize.width - 40) / 12);
876         } else {
877             runtime.gridMaxPoints = monitorSettings.gridMaxPoints;
878         }
880         runtime.xmin = new Date().getTime() - server_time_diff - runtime.gridMaxPoints * monitorSettings.gridRefresh;
881         runtime.xmax = new Date().getTime() - server_time_diff + monitorSettings.gridRefresh;
883         /* Calculate how much spacing there is between each chart */
884         $('table#chartGrid').html('<tr><td></td><td></td></tr><tr><td></td><td></td></tr>');
885         chartSpacing = {
886             width: $('table#chartGrid td:nth-child(2)').offset().left 
887                     - $('table#chartGrid td:nth-child(1)').offset().left,
888             height: $('table#chartGrid tr:nth-child(2) td:nth-child(2)').offset().top 
889                     - $('table#chartGrid tr:nth-child(1) td:nth-child(1)').offset().top
890         }
891         $('table#chartGrid').html('');
892         
893         /* Add all charts - in correct order */
894         var keys = [];
895         $.each(runtime.charts, function(key, value) {
896             keys.push(key);
897         });
898         keys.sort();
899         for (var i = 0; i<keys.length; i++)
900             addChart(runtime.charts[keys[i]], true);
902         /* Fill in missing cells */
903         var numCharts = $('table#chartGrid .monitorChart').length;
904         var numMissingCells = (monitorSettings.columns - numCharts % monitorSettings.columns) % monitorSettings.columns;
905         for (var i = 0; i < numMissingCells; i++) {
906             $('table#chartGrid tr:last').append('<td></td>');
907         }
909         // Empty cells should keep their size so you can drop onto them
910         $('table#chartGrid tr td').css('width', chartSize().width + 'px');
911         
912         buildRequiredDataList();
913         refreshChartGrid();
914     }
915     
916     /* Destroys all monitor related resources */
917     function destroyGrid() {
918         if (runtime.charts) {
919             $.each(runtime.charts, function(key, value) {
920                 try {
921                     value.chart.destroy();
922                 } catch(err) {}
923             });
924         }
925         
926         try {
927             runtime.refreshRequest.abort();
928         } catch(err) {}
929         try {    
930             clearTimeout(runtime.refreshTimeout);
931         } catch(err) {}
932             
933         $('table#chartGrid').html('');
935         runtime.charts = null;
936         runtime.chartAI = 0;
937         monitorSettings = null;
938     }
939     
940     /* Calls destroyGrid() and initGrid(), but before doing so it saves the chart 
941      * data from each chart and restores it after the monitor is initialized again */
942     function rebuildGrid() {
943         var oldData = null;
944         if (runtime.charts) {
945             oldData = {};
946             $.each(runtime.charts, function(key, chartObj) {
947                 for (var i = 0; i < chartObj.nodes.length; i++) {
948                     oldData[chartObj.nodes[i].dataPoint] = [];
949                     for (var j = 0; j < chartObj.chart.series[i].data.length; j++)
950                         oldData[chartObj.nodes[i].dataPoint].push([chartObj.chart.series[i].data[j].x, chartObj.chart.series[i].data[j].y]);
951                 }
952             });
953         }
954         
955         destroyGrid();
956         initGrid();
957         
958         if (oldData) {
959             $.each(runtime.charts, function(key, chartObj) {
960                 for (var j = 0; j < chartObj.nodes.length; j++) {
961                     if (oldData[chartObj.nodes[j].dataPoint]) {
962                         chartObj.chart.series[j].setData(oldData[chartObj.nodes[j].dataPoint]);
963                     }
964                 }
965             });
966         }      
967     }
969     /* Calculactes the dynamic chart size that depends on the column width */
970     function chartSize() {
971         var wdt = $('div#logTable').innerWidth() / monitorSettings.columns - (monitorSettings.columns - 1) * chartSpacing.width;
972         return {
973             width: wdt,
974             height: 0.75 * wdt
975         }
976     }
978     /* Adds a chart to the chart grid */
979     function addChart(chartObj, initialize) {
980         series = [];
981         for (var j = 0; j<chartObj.nodes.length; j++)
982             series.push(chartObj.nodes[j]);
984         settings = {
985             chart: {
986                 renderTo: 'gridchart' + runtime.chartAI,
987                 width: chartSize().width,
988                 height: chartSize().height,
989                 marginRight: 5,
990                 zoomType: 'x',
991                 events: {
992                     selection: function(event) {
993                         if (editMode || $('#logAnalyseDialog').length == 0) {
994                             return false;
995                         }
997                         var extremesObject = event.xAxis[0],
998                             min = extremesObject.min,
999                             max = extremesObject.max;
1001                         $('#logAnalyseDialog input[name="dateStart"]')
1002                             .attr('value', Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', new Date(min)));
1003                         $('#logAnalyseDialog input[name="dateEnd"]')
1004                             .attr('value', Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', new Date(max)));
1006                         var dlgBtns = { };
1008                         dlgBtns[PMA_messages['strFromSlowLog']] = function() {
1009                             loadLog('slow');
1010                             $(this).dialog("close");
1011                         };
1012                         
1013                         dlgBtns[PMA_messages['strFromGeneralLog']] = function() {
1014                             loadLog('general');
1015                             $(this).dialog("close");
1016                         };
1017                         
1018                         function loadLog(type) {
1019                             var dateStart = Date.parse($('#logAnalyseDialog input[name="dateStart"]').prop('value')) || min;
1020                             var dateEnd = Date.parse($('#logAnalyseDialog input[name="dateEnd"]').prop('value')) || max;
1022                             loadLogStatistics({
1023                                 src: type,
1024                                 start: dateStart,
1025                                 end: dateEnd,
1026                                 removeVariables: $('input#removeVariables').prop('checked'),
1027                                 limitTypes: $('input#limitTypes').prop('checked')
1028                             });
1029                         }
1030                         
1031                         $('#logAnalyseDialog').dialog({
1032                             width: 'auto',
1033                             height: 'auto',
1034                             buttons: dlgBtns
1035                         });
1037                         return false;
1038                     }
1039                 }
1040             },
1041             xAxis: {
1042                 min: runtime.xmin,
1043                 max: runtime.xmax
1044             },
1046             yAxis: {
1047                 title: {
1048                     text: ''
1049                 }
1050             },
1051             tooltip: {
1052                 formatter: function() {
1053                         var s = '<b>' + Highcharts.dateFormat('%H:%M:%S', this.x) + '</b>';
1055                         $.each(this.points, function(i, point) {
1056                             s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + ':</span> ' +
1057                                 ((parseInt(point.y) == point.y) ? point.y : Highcharts.numberFormat(this.y, 2)) + ' ' + (point.series.options.unit || '');
1058                         });
1060                         return s;
1061                 },
1062                 shared: true
1063             },
1064             legend: {
1065                 enabled: false
1066             },
1067             series: series,
1068             buttons: gridbuttons,
1069             title: { text: chartObj.title }
1070         };
1072         if (chartObj.settings) {
1073             $.extend(true, settings, chartObj.settings);
1074         }
1076         if ($('#' + settings.chart.renderTo).length == 0) {
1077             var numCharts = $('table#chartGrid .monitorChart').length;
1079             if (numCharts == 0 || !( numCharts % monitorSettings.columns)) {
1080                 $('table#chartGrid').append('<tr></tr>');
1081             }
1083             $('table#chartGrid tr:last').append('<td><div class="ui-state-default monitorChart" id="' + settings.chart.renderTo + '"></div></td>');
1084         }
1086         chartObj.chart = PMA_createChart(settings);
1087         chartObj.numPoints = 0;
1088         
1089         if (initialize != true) {
1090             runtime.charts['c' + runtime.chartAI] = chartObj;
1091             buildRequiredDataList();
1092         }
1094         // Edit, Print icon only in edit mode
1095         $('table#chartGrid div svg').find('*[zIndex=20], *[zIndex=21], *[zIndex=19]').toggle(editMode)
1097         runtime.chartAI++;
1098     }
1100     /* Opens a dialog that allows one to edit the title and series labels of the supplied chart */
1101     function editChart(chartObj) {
1102         var htmlnode = chartObj.options.chart.renderTo;
1103         if (! htmlnode ) {
1104             return;
1105         }
1106         
1107         var chart = null;
1108         var chartKey = null;
1109         $.each(runtime.charts, function(key, value) {
1110             if (value.chart.options.chart.renderTo == htmlnode) {
1111                 chart = value;
1112                 chartKey = key;
1113                 return false;
1114             }
1115         });
1116         
1117         if (chart == null) {
1118             return;
1119         }
1121         var htmlStr = '<p><b>Chart title: </b> <br/> <input type="text" size="35" name="chartTitle" value="' + chart.title + '" />';
1122         htmlStr += '</p><p><b>Series:</b> </p><ol>';
1123         for (var i = 0; i<chart.nodes.length; i++) {
1124             htmlStr += '<li><i>' + chart.nodes[i].dataPoints[0].name  + ': </i><br/><input type="text" name="chartSerie-' + i + '" value="' + chart.nodes[i].name + '" /></li>';
1125         }
1126         
1127         dlgBtns = {};
1128         dlgBtns['Save'] = function() {
1129             runtime.charts[chartKey].title = $('div#emptyDialog input[name="chartTitle"]').attr('value');
1130             runtime.charts[chartKey].chart.setTitle({ text: runtime.charts[chartKey].title });
1131             
1132             $('div#emptyDialog input[name*="chartSerie"]').each(function() {
1133                 var idx = $(this).attr('name').split('-')[1];
1134                 runtime.charts[chartKey].nodes[idx].name = $(this).attr('value');
1135                 runtime.charts[chartKey].chart.series[idx].name = $(this).attr('value');
1136             });
1137             
1138             $(this).dialog('close');
1139             saveMonitor();
1140         };
1141         dlgBtns['Cancel'] = function() {
1142             $(this).dialog('close');
1143         };
1144         
1145         $('div#emptyDialog').attr('title', 'Edit chart');
1146         $('div#emptyDialog').html(htmlStr + '</ol>');
1147         $('div#emptyDialog').dialog({
1148             width: 'auto',
1149             height: 'auto',
1150             buttons: dlgBtns
1151         });
1152     }
1153     
1154     /* Removes a chart from the grid */
1155     function removeChart(chartObj) {
1156         var htmlnode = chartObj.options.chart.renderTo;
1157         if (! htmlnode ) {
1158             return;
1159         }
1160         
1161         $.each(runtime.charts, function(key, value) {
1162             if (value.chart.options.chart.renderTo == htmlnode) {
1163                 delete runtime.charts[key];
1164                 return false;
1165             }
1166         });
1168         buildRequiredDataList();
1170         // Using settimeout() because clicking the remove link fires an onclick event
1171         // which throws an error when the chart is destroyed
1172         setTimeout(function() {
1173             chartObj.destroy();
1174             $('div#' + htmlnode).remove();
1175         }, 10);
1177         saveMonitor(); // Save settings
1178     }
1180     /* Called in regular intervalls, this function updates the values of each chart in the grid */
1181     function refreshChartGrid() {
1182         /* Send to server */
1183         runtime.refreshRequest = $.post('server_status.php?' + url_query, {
1184             ajax_request: true, 
1185             chart_data: 1, 
1186             type: 'chartgrid', 
1187             requiredData: $.toJSON(runtime.dataList) 
1188         }, function(data) {
1189             var chartData;
1190             try {
1191                 chartData = $.parseJSON(data);
1192             } catch(err) {
1193                 return serverResponseError();
1194             }
1195             var value, i = 0;
1196             var diff;
1197             
1198             /* Update values in each graph */
1199             $.each(runtime.charts, function(orderKey, elem) {
1200                 var key = elem.chartID;
1201                 // If newly added chart, we have no data for it yet
1202                 if (! chartData[key]) {
1203                     return;
1204                 }
1205                 // Draw all series
1206                 for (var j = 0; j < elem.nodes.length; j++) {
1207                     // Update x-axis
1208                     if (i == 0 && j == 0) {
1209                         if (oldChartData == null) {
1210                             diff = chartData.x - runtime.xmax;
1211                         } else {
1212                             diff = parseInt(chartData.x - oldChartData.x);
1213                         }
1215                         runtime.xmin += diff;
1216                         runtime.xmax += diff;
1217                     }
1219                     elem.chart.xAxis[0].setExtremes(runtime.xmin, runtime.xmax, false);
1221                     /* Calculate y value */
1222                     
1223                     // If transform function given, use it
1224                     if (elem.nodes[j].transformFn) {
1225                         value = chartValueTransform(
1226                             elem.nodes[j].transformFn,
1227                             chartData[key][j],
1228                             // Check if first iteration (oldChartData==null), or if newly added chart oldChartData[key]==null
1229                             (oldChartData == null || oldChartData[key] == null ? null : oldChartData[key][j])
1230                         );
1232                     // Otherwise use original value and apply differential and divisor if given,
1233                     // in this case we have only one data point per series - located at chartData[key][j][0]
1234                     } else {
1235                         value = parseFloat(chartData[key][j][0].value);
1237                         if (elem.nodes[j].display == 'differential') {
1238                             if (oldChartData == null || oldChartData[key] == null) { 
1239                                 continue;
1240                             }
1241                             value -= oldChartData[key][j][0].value;
1242                         }
1244                         if (elem.nodes[j].valueDivisor) {
1245                             value = value / elem.nodes[j].valueDivisor;
1246                         }
1247                     }
1248                     
1249                     // Set y value, if defined
1250                     if (value != undefined) {
1251                         elem.chart.series[j].addPoint(
1252                             { x: chartData.x, y: value },
1253                             false,
1254                             elem.numPoints >= runtime.gridMaxPoints
1255                         );
1256                     }
1257                 }
1259                 i++;
1261                 runtime.charts[orderKey].numPoints++;
1262                 if (runtime.redrawCharts) {
1263                     elem.chart.redraw();
1264                 }
1265             });
1267             oldChartData = chartData;
1269             runtime.refreshTimeout = setTimeout(refreshChartGrid, monitorSettings.gridRefresh);
1270         });
1271     }
1272     
1273     /* Function that supplies special value transform functions for chart values */
1274     function chartValueTransform(name, cur, prev) {        
1275         switch(name) {
1276         case 'cpu-linux':
1277             if (prev == null) {
1278                 return undefined;
1279             }
1280             // cur and prev are datapoint arrays, but containing only 1 element for cpu-linux
1281             cur = cur[0], prev = prev[0];
1283             var diff_total = cur.busy + cur.idle - (prev.busy + prev.idle);
1284             var diff_idle = cur.idle - prev.idle;
1285             return 100 * (diff_total - diff_idle) / diff_total;
1287         // Query cache efficiency (%)
1288         case 'qce':
1289             if (prev == null) {
1290                 return undefined;
1291             }
1292             // cur[0].value is Qcache_hits, cur[1].value is Com_select
1293             var diffQHits = cur[0].value - prev[0].value;
1294             // No NaN please :-)
1295             if (cur[1].value - prev[1].value == 0) return 0;
1297             return diffQHits / (cur[1].value - prev[1].value + diffQHits) * 100;
1299         // Query cache usage (%)
1300         case 'qcu':
1301             if (cur[1].value == 0) return 0;
1302             // cur[0].value is Qcache_free_memory, cur[1].value is query_cache_size
1303             return 100 - cur[0].value / cur[1].value * 100;
1305         }
1306         return undefined;
1307     }
1309     /* Build list of nodes that need to be retrieved from server.
1310      * It creates something like a stripped down version of the runtime.charts object.
1311      */
1312     function buildRequiredDataList() {
1313         runtime.dataList = {};
1314         // Store an own id, because the property name is subject of reordering, 
1315         // thus destroying our mapping with runtime.charts <=> runtime.dataList
1316         var chartID = 0;
1317         $.each(runtime.charts, function(key, chart) {
1318             runtime.dataList[chartID] = [];
1319             for(var i=0; i < chart.nodes.length; i++) {
1320                 runtime.dataList[chartID][i] = chart.nodes[i].dataPoints;
1321             }
1322             runtime.charts[key].chartID = chartID;
1323             chartID++;
1324         });
1325     }
1327     /* Loads the log table data, generates the table and handles the filters */
1328     function loadLogStatistics(opts) {
1329         var tableStr = '';
1330         var logRequest = null;
1332         if (! opts.removeVariables) {
1333             opts.removeVariables = false;
1334         }
1335         if (! opts.limitTypes) {
1336             opts.limitTypes = false;
1337         }
1338         
1339         $('#emptyDialog').html(PMA_messages['strAnalysingLogs'] + 
1340                                 ' <img class="ajaxIcon" src="' + pmaThemeImage + 
1341                                 'ajax_clock_small.gif" alt="">');
1343         $('#emptyDialog').dialog({
1344             width: 'auto',
1345             height: 'auto',
1346             buttons: {
1347                 'Cancel request': function() {
1348                     if (logRequest != null) {
1349                         logRequest.abort();
1350                     }
1352                     $(this).dialog("close");
1353                 }
1354             }
1355         });
1358         logRequest = $.get('server_status.php?' + url_query,
1359             {   ajax_request: true,
1360                 log_data: 1,
1361                 type: opts.src,
1362                 time_start: Math.round(opts.start / 1000),
1363                 time_end: Math.round(opts.end / 1000),
1364                 removeVariables: opts.removeVariables,
1365                 limitTypes: opts.limitTypes
1366             },
1367             function(data) { 
1368                 var logData;
1369                 try {
1370                     logData = $.parseJSON(data);
1371                 } catch(err) {
1372                     return serverResponseError();
1373                 }
1374                 
1375                 if (logData.rows.length != 0) {
1376                     runtime.logDataCols = buildLogTable(logData);
1378                     /* Show some stats in the dialog */
1379                     $('#emptyDialog').attr('title', PMA_messages['strLoadingLogs']);
1380                     $('#emptyDialog').html('<p>' + PMA_messages['strLogDataLoaded'] + '</p>');
1381                     $.each(logData.sum, function(key, value) {
1382                         key = key.charAt(0).toUpperCase() + key.slice(1).toLowerCase();
1383                         if (key == 'Total') {
1384                             key = '<b>' + key + '</b>';
1385                         }
1386                         $('#emptyDialog').append(key + ': ' + value + '<br/>');
1387                     });
1389                     /* Add filter options if more than a bunch of rows there to filter */
1390                     if (logData.numRows > 12) {
1391                         $('div#logTable').prepend(
1392                             '<fieldset id="logDataFilter">' +
1393                             '   <legend>' + PMA_messages['strFiltersForLogTable'] + '</legend>' +
1394                             '   <div class="formelement">' +
1395                             '           <label for="filterQueryText">' + PMA_messages['strFilterByWordRegexp'] + '</label>' +
1396                             '           <input name="filterQueryText" type="text" id="filterQueryText" style="vertical-align: baseline;" />' +
1397                             '   </div>' +
1398                             ((logData.numRows > 250) ? ' <div class="formelement"><button name="startFilterQueryText" id="startFilterQueryText">' + PMA_messages['strFilter'] + '</button></div>' : '') +
1399                             '   <div class="formelement">' +
1400                             '       <input type="checkbox" id="noWHEREData" name="noWHEREData" value="1" /> ' +
1401                             '       <label for="noWHEREData"> ' + PMA_messages['strIgnoreWhereAndGroup'] + '</label>' +
1402                             '   </div' +
1403                             '</fieldset>'
1404                         );
1406                         $('div#logTable input#noWHEREData').change(function() {
1407                             filterQueries(true);
1408                         });
1410                         if (logData.numRows > 250) {
1411                             $('div#logTable button#startFilterQueryText').click(filterQueries);
1412                         } else {
1413                             $('div#logTable input#filterQueryText').keyup(filterQueries);
1414                         }
1416                     }
1418                     var dlgBtns = {};
1419                     dlgBtns[PMA_messages['strJumpToTable']] = function() {
1420                         $(this).dialog("close");
1421                         $(document).scrollTop($('div#logTable').offset().top);
1422                     };
1423                     
1424                     $('#emptyDialog').dialog( "option", "buttons", dlgBtns);
1425                     
1426                 } else {
1427                     $('#emptyDialog').html('<p>' + PMA_messages['strNoDataFound'] + '</p>');
1428                     
1429                     var dlgBtns = {};
1430                     dlgBtns[PMA_messages['strClose']] = function() { 
1431                         $(this).dialog("close"); 
1432                     };
1433                     
1434                     $('#emptyDialog').dialog( "option", "buttons", dlgBtns );
1435                 }
1436             }
1437         );
1439         /* Handles the actions performed when the user uses any of the log table filters 
1440          * which are the filter by name and grouping with ignoring data in WHERE clauses
1441          * 
1442          * @param boolean Should be true when the users enabled or disabled to group queries ignoring data in WHERE clauses
1443         */
1444         function filterQueries(varFilterChange) {
1445             var odd_row = false, cell, textFilter;
1446             var val = $('div#logTable input#filterQueryText').val();
1448             if (val.length == 0) {
1449                 textFilter = null;
1450             } else {
1451                 textFilter = new RegExp(val, 'i');
1452             }
1453             
1454             var rowSum = 0, totalSum = 0, i = 0, q;
1455             var noVars = $('div#logTable input#noWHEREData').attr('checked');
1456             var equalsFilter = /([^=]+)=(\d+|((\'|"|).*?[^\\])\4((\s+)|$))/gi;
1457             var functionFilter = /([a-z0-9_]+)\(.+?\)/gi;
1458             var filteredQueries = {}, filteredQueriesLines = {};
1459             var hide = false, rowData;
1460             var queryColumnName = runtime.logDataCols[runtime.logDataCols.length - 2];
1461             var sumColumnName = runtime.logDataCols[runtime.logDataCols.length - 1];
1462             var isSlowLog = opts.src == 'slow';
1463             var columnSums = {};
1464             
1465             // For the slow log we have to count many columns (query_time, lock_time, rows_examined, rows_sent, etc.)
1466             var countRow = function(query, row) {
1467                 var cells = row.match(/<td>(.*?)<\/td>/gi);
1468                 if (!columnSums[query]) {
1469                     columnSums[query] = [0, 0, 0, 0];
1470                 }
1472                 // lock_time and query_time and displayed in timespan format
1473                 columnSums[query][0] += timeToSec(cells[2].replace(/(<td>|<\/td>)/gi, ''));
1474                 columnSums[query][1] += timeToSec(cells[3].replace(/(<td>|<\/td>)/gi, ''));
1475                 // rows_examind and rows_sent are just numbers
1476                 columnSums[query][2] += parseInt(cells[4].replace(/(<td>|<\/td>)/gi, ''));
1477                 columnSums[query][3] += parseInt(cells[5].replace(/(<td>|<\/td>)/gi, ''));
1478             };
1479             
1480             // We just assume the sql text is always in the second last column, and that the total count is right of it
1481             $('div#logTable table tbody tr td:nth-child(' + (runtime.logDataCols.length - 1) + ')').each(function() {
1482                 // If query is a SELECT and user enabled or disabled to group queries ignoring data in where statements, we 
1483                 // need to re-calculate the sums of each row
1484                 if (varFilterChange && $(this).html().match(/^SELECT/i)) {
1485                     if (noVars) {
1486                         // Group on => Sum up identical columns, and hide all but 1
1487                         
1488                         q = $(this).text().replace(equalsFilter, '$1=...$6').trim();
1489                         q = q.replace(functionFilter, ' $1(...)');
1491                         // Js does not specify a limit on property name length, so we can abuse it as index :-)
1492                         if (filteredQueries[q]) {
1493                             filteredQueries[q] += parseInt($(this).next().text());
1494                             totalSum += parseInt($(this).next().text());
1495                             hide = true;
1496                         } else {
1497                             filteredQueries[q] = parseInt($(this).next().text());;
1498                             filteredQueriesLines[q] = i;
1499                             $(this).text(q);
1500                         }
1501                         if (isSlowLog) {
1502                             countRow(q, $(this).parent().html());
1503                         }
1505                     } else {
1506                         // Group off: Restore original columns
1508                         rowData = $(this).parent().data('query');
1509                         // Restore SQL text
1510                         $(this).text(rowData[queryColumnName]);
1511                         // Restore total count
1512                         $(this).next().text(rowData[sumColumnName]);
1513                         // Restore slow log columns
1514                         if (isSlowLog) {
1515                             $(this).parent().children('td:nth-child(3)').text(rowData['query_time']);
1516                             $(this).parent().children('td:nth-child(4)').text(rowData['lock_time']);
1517                             $(this).parent().children('td:nth-child(5)').text(rowData['rows_sent']);
1518                             $(this).parent().children('td:nth-child(6)').text(rowData['rows_examined']);
1519                         }
1520                     }
1521                 }
1523                 // If not required to be hidden, do we need to hide because of a not matching text filter?
1524                 if (! hide && (textFilter != null && ! textFilter.exec($(this).text()))) {
1525                     hide = true;
1526                 }
1528                 // Now display or hide this column
1529                 if (hide) {
1530                     $(this).parent().css('display', 'none');
1531                 } else {
1532                     totalSum += parseInt($(this).next().text());
1533                     rowSum++;
1535                     odd_row = ! odd_row;
1536                     $(this).parent().css('display', '');
1537                     if (odd_row) {
1538                         $(this).parent().addClass('odd');
1539                         $(this).parent().removeClass('even');
1540                     } else {
1541                         $(this).parent().addClass('even');
1542                         $(this).parent().removeClass('odd');
1543                     }
1544                 }
1546                 hide = false;
1547                 i++;
1548             });
1549                        
1550             // We finished summarizing counts => Update count values of all grouped entries
1551             if (varFilterChange) {
1552                 if (noVars) {
1553                     var numCol, row, $table = $('div#logTable table tbody');
1554                     $.each(filteredQueriesLines, function(key, value) {
1555                         if (filteredQueries[key] <= 1) {
1556                             return;
1557                         }
1558                         
1559                         row =  $table.children('tr:nth-child(' + (value + 1) + ')');
1560                         numCol = row.children(':nth-child(' + (runtime.logDataCols.length) + ')');
1561                         numCol.text(filteredQueries[key]);
1562                         
1563                         if (isSlowLog) {
1564                             row.children('td:nth-child(3)').text(secToTime(columnSums[key][0]));
1565                             row.children('td:nth-child(4)').text(secToTime(columnSums[key][1]));
1566                             row.children('td:nth-child(5)').text(columnSums[key][2]);
1567                             row.children('td:nth-child(6)').text(columnSums[key][3]);
1568                         }
1569                     });
1570                 }
1571                 
1572                 $('div#logTable table').trigger("update"); 
1573                 setTimeout(function() {                    
1574                     $('div#logTable table').trigger('sorton', [[[runtime.logDataCols.length - 1, 1]]]);
1575                 }, 0);
1576             }
1578             // Display some stats at the bottom of the table
1579             $('div#logTable table tfoot tr')
1580                 .html('<th colspan="' + (runtime.logDataCols.length - 1) + '">' +
1581                       PMA_messages['strSumRows'] + ' ' + rowSum + '<span style="float:right">' +
1582                       PMA_messages['strTotal'] + '</span></th><th align="right">' + totalSum + '</th>');
1583         }
1584     }
1586     /* Turns a timespan (12:12:12) into a number */
1587     function timeToSec(timeStr) {
1588         var time = timeStr.split(':');
1589         return parseInt(time[0]*3600) + parseInt(time[1]*60) + parseInt(time[2]);
1590     }
1591     
1592     /* Turns a number into a timespan (100 into 00:01:40) */
1593     function secToTime(timeInt) {
1594         hours = Math.floor(timeInt / 3600);
1595         timeInt -= hours*3600;
1596         minutes = Math.floor(timeInt / 60);
1597         timeInt -= minutes*60;
1598         
1599         if (hours < 10) {
1600             hours = '0' + hours;
1601         }
1602         if (minutes < 10) {
1603             minutes = '0' + minutes;
1604         }
1605         if (timeInt < 10) {
1606             timeInt = '0' + timeInt;
1607         }
1608         
1609         return hours + ':' + minutes + ':' + timeInt;
1610     }
1611     
1612     /* Constructs the log table out of the retrieved server data */
1613     function buildLogTable(data) {
1614         var rows = data.rows;
1615         var cols = new Array();
1616         var $table = $('<table border="0" class="sortable"></table>');
1617         var $tBody, $tRow, $tCell;
1619         $('#logTable').html($table);
1621         var formatValue = function(name, value) {
1622             switch(name) {
1623                 case 'user_host':
1624                     return value.replace(/(\[.*?\])+/g, '');
1625             }
1626             return value;
1627         };
1628         
1629         for (var i = 0; i < rows.length; i++) {
1630             if (i == 0) {
1631                 $.each(rows[0], function(key, value) {
1632                     cols.push(key);
1633                 });
1634                 $table.append( '<thead>' +
1635                                '<tr><th class="nowrap">' + cols.join('</th><th class="nowrap">') + '</th></tr>' +
1636                                '</thead>');
1638                 $table.append($tBody = $('<tbody></tbody>'));
1639             }
1641             $tBody.append($tRow = $('<tr class="noclick"></tr>'));
1642             var cl = '';
1643             for (var j = 0; j < cols.length; j++) {
1644                 // Assuming the query column is the second last
1645                 if (j == cols.length - 2 && rows[i][cols[j]].match(/^SELECT/i)) {
1646                     $tRow.append($tCell = $('<td class="linkElem">' + formatValue(cols[j], rows[i][cols[j]]) + '</td>'));
1647                     $tCell.click(openQueryAnalyzer);
1648                 } else
1649                     $tRow.append('<td>' + formatValue(cols[j], rows[i][cols[j]]) + '</td>');
1652                 $tRow.data('query', rows[i]);
1653             }
1654         }
1656         $table.append('<tfoot>' +
1657                     '<tr><th colspan="' + (cols.length - 1) + '">' + PMA_messages['strSumRows'] +
1658                     ' ' + data.numRows + '<span style="float:right">' + PMA_messages['strTotal'] +
1659                     '</span></th><th align="right">' + data.sum.TOTAL + '</th></tr></tfoot>');
1661         // Append a tooltip to the count column, if there exist one
1662         if ($('#logTable th:last').html() == '#') {
1663             $('#logTable th:last').append('&nbsp;<img class="qroupedQueryInfoIcon icon ic_b_docs" src="themes/dot.gif" alt="" />');
1665             var qtipContent = PMA_messages['strCountColumnExplanation'];
1666             if (groupInserts) {
1667                 qtipContent += '<p>' + PMA_messages['strMoreCountColumnExplanation'] + '</p>';
1668             }
1670             $('img.qroupedQueryInfoIcon').qtip({
1671                 content: qtipContent,
1672                 position: {
1673                     corner: {
1674                         target: 'bottomMiddle',
1675                         tooltip: 'topRight'
1676                     }
1678                 },
1679                 hide: { delay: 1000 }
1680             })
1681         }
1683         $('div#logTable table').tablesorter({
1684             sortList: [[cols.length - 1, 1]],
1685             widgets: ['fast-zebra']
1686         });
1688         $('div#logTable table thead th')
1689             .append('<img class="icon sortableIcon" src="themes/dot.gif" alt="">');
1691         return cols;
1692     }
1693         
1694     /* Opens the query analyzer dialog */
1695     function openQueryAnalyzer() {
1696         var rowData = $(this).parent().data('query');
1697         var query = rowData.argument || rowData.sql_text;
1699         query = PMA_SQLPrettyPrint(query);
1700         codemirror_editor.setValue(query);
1701         // Codemirror is bugged, it doesn't refresh properly sometimes. Following lines seem to fix that
1702         setTimeout(function() {
1703             codemirror_editor.refresh()
1704         },50);
1706         var profilingChart = null;
1707         var dlgBtns = {};
1708         
1709         dlgBtns[PMA_messages['strAnalyzeQuery']] = function() { 
1710             loadQueryAnalysis(rowData); 
1711         };
1712         dlgBtns[PMA_messages['strClose']] = function() { 
1713             if (profilingChart != null) {
1714                 profilingChart.destroy();
1715             }
1716             $('div#queryAnalyzerDialog div.placeHolder').html('');
1717             codemirror_editor.setValue('');
1718             $(this).dialog("close");
1719         };
1721         $('div#queryAnalyzerDialog').dialog({
1722             width: 'auto',
1723             height: 'auto',
1724             resizable: false,
1725             buttons: dlgBtns
1726         });
1727     }
1728     
1729     /* Loads and displays the analyzed query data */
1730     function loadQueryAnalysis(rowData) {
1731         var db = rowData.db || '';
1732         
1733         $('div#queryAnalyzerDialog div.placeHolder').html(
1734             'Analyzing... ' + '<img class="ajaxIcon" src="' + 
1735             pmaThemeImage + 'ajax_clock_small.gif" alt="">');
1737         $.post('server_status.php?' + url_query, {
1738             ajax_request: true,
1739             query_analyzer: true,
1740             query: codemirror_editor.getValue(),
1741             database: db
1742         }, function(data) {
1743             data = $.parseJSON(data);
1744             var totalTime = 0;
1746             if (data.error) {
1747                 $('div#queryAnalyzerDialog div.placeHolder').html('<div class="error">' + data.error + '</div>');
1748                 return;
1749             }
1751             // Float sux, I'll use table :(
1752             $('div#queryAnalyzerDialog div.placeHolder')
1753                 .html('<table width="100%" border="0"><tr><td class="explain"></td><td class="chart"></td></tr></table>');
1754             
1755             var explain = '<b>Explain output</b> ' + explain_docu;
1756             if (data.explain.length > 1) {
1757                 explain += ' (';
1758                 for (var i = 0; i < data.explain.length; i++) {
1759                     if (i > 0) {
1760                         explain += ', ';
1761                     }
1762                     explain += '<a href="#showExplain-' + i + '">' + i + '</a>';
1763                 }
1764                 explain += ')';
1765             }
1766             explain += '<p></p>';
1767             for (var i = 0; i < data.explain.length; i++) {
1768                 explain += '<div class="explain-' + i + '"' + (i>0? 'style="display:none;"' : '' ) + '>';
1769                 $.each(data.explain[i], function(key, value) {
1770                     value = (value == null)?'null':value;
1771                     
1772                     if (key == 'type' && value.toLowerCase() == 'all') {
1773                         value = '<span class="attention">' + value + '</span>';
1774                     }
1775                     if (key == 'Extra') {
1776                         value = value.replace(/(using (temporary|filesort))/gi, '<span class="attention">$1</span>');
1777                     }
1778                     explain += key + ': ' + value + '<br />';
1779                 });
1780                 explain += '</div>';
1781             }
1782             
1783             explain += '<p><b>' + PMA_messages['strAffectedRows'] + '</b> ' + data.affectedRows;
1785             $('div#queryAnalyzerDialog div.placeHolder td.explain').append(explain);
1786             
1787             $('div#queryAnalyzerDialog div.placeHolder a[href*="#showExplain"]').click(function() {
1788                 var id = $(this).attr('href').split('-')[1];
1789                 $(this).parent().find('div[class*="explain"]').hide();
1790                 $(this).parent().find('div[class*="explain-' + id + '"]').show();
1791             });
1792             
1793             if (data.profiling) {
1794                 var chartData = [];
1795                 var numberTable = '<table class="queryNums"><thead><tr><th>Status</th><th>Time</th></tr></thead><tbody>';
1796                 var duration;
1798                 for (var i = 0; i < data.profiling.length; i++) {
1799                     duration = parseFloat(data.profiling[i].duration);
1801                     chartData.push([data.profiling[i].state, duration]);
1802                     totalTime += duration;
1804                     numberTable += '<tr><td>' + data.profiling[i].state + ' </td><td> ' + PMA_prettyProfilingNum(duration, 2) + '</td></tr>';
1805                 }
1806                 numberTable += '<tr><td><b>Total time:</b></td><td>' + PMA_prettyProfilingNum(totalTime, 2) + '</td></tr>';
1807                 numberTable += '</tbody></table>';
1808                 
1809                 $('div#queryAnalyzerDialog div.placeHolder td.chart').append('<b>Profiling results ' + profiling_docu + '</b> (<a href="#showNums">Table</a>, <a href="#showChart">Chart</a>)<br/>' + numberTable + ' <div id="queryProfiling"></div>');
1810                 
1811                 $('div#queryAnalyzerDialog div.placeHolder a[href="#showNums"]').click(function() {
1812                     $('div#queryAnalyzerDialog div#queryProfiling').hide();
1813                     $('div#queryAnalyzerDialog table.queryNums').show();
1814                     return false;
1815                 });
1817                 $('div#queryAnalyzerDialog div.placeHolder a[href="#showChart"]').click(function() {
1818                     $('div#queryAnalyzerDialog div#queryProfiling').show();
1819                     $('div#queryAnalyzerDialog table.queryNums').hide();
1820                     return false;
1821                 });
1823                 profilingChart = PMA_createProfilingChart(chartData, {
1824                     chart: {
1825                         renderTo: 'queryProfiling'
1826                     },
1827                     plotOptions: {
1828                         pie: {
1829                             size: '50%'
1830                         }
1831                     }
1832                 });
1835                 $('div#queryProfiling').resizable();
1836             }
1837         });
1838     }
1840     /* Saves the monitor to localstorage */
1841     function saveMonitor() {
1842         var gridCopy = {};
1844         $.each(runtime.charts, function(key, elem) {
1845             gridCopy[key] = {};
1846             gridCopy[key].nodes = elem.nodes;
1847             gridCopy[key].settings = elem.settings;
1848             gridCopy[key].title = elem.title;
1849         });
1851         if (window.localStorage) {
1852             window.localStorage['monitorCharts'] = $.toJSON(gridCopy);
1853             window.localStorage['monitorSettings'] = $.toJSON(monitorSettings);
1854             window.localStorage['monitorVersion'] = monitorProtocolVersion;
1855         }
1857         $('a[href="#clearMonitorConfig"]').show();
1858     }
1861 // Run the monitor once loaded
1862 $(function() {
1863     $('a[href="#pauseCharts"]').trigger('click');