From 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 9 Jan 2020 10:55:03 +0100 Subject: phpmyadmin working --- srcs/phpmyadmin/js/server/databases.js | 152 ++ srcs/phpmyadmin/js/server/plugins.js | 16 + srcs/phpmyadmin/js/server/privileges.js | 494 ++++++ srcs/phpmyadmin/js/server/status/advisor.js | 100 ++ srcs/phpmyadmin/js/server/status/monitor.js | 2219 +++++++++++++++++++++++++ srcs/phpmyadmin/js/server/status/processes.js | 189 +++ srcs/phpmyadmin/js/server/status/queries.js | 46 + srcs/phpmyadmin/js/server/status/sorter.js | 71 + srcs/phpmyadmin/js/server/status/variables.js | 100 ++ srcs/phpmyadmin/js/server/user_groups.js | 52 + srcs/phpmyadmin/js/server/variables.js | 118 ++ 11 files changed, 3557 insertions(+) create mode 100644 srcs/phpmyadmin/js/server/databases.js create mode 100644 srcs/phpmyadmin/js/server/plugins.js create mode 100644 srcs/phpmyadmin/js/server/privileges.js create mode 100644 srcs/phpmyadmin/js/server/status/advisor.js create mode 100644 srcs/phpmyadmin/js/server/status/monitor.js create mode 100644 srcs/phpmyadmin/js/server/status/processes.js create mode 100644 srcs/phpmyadmin/js/server/status/queries.js create mode 100644 srcs/phpmyadmin/js/server/status/sorter.js create mode 100644 srcs/phpmyadmin/js/server/status/variables.js create mode 100644 srcs/phpmyadmin/js/server/user_groups.js create mode 100644 srcs/phpmyadmin/js/server/variables.js (limited to 'srcs/phpmyadmin/js/server') diff --git a/srcs/phpmyadmin/js/server/databases.js b/srcs/phpmyadmin/js/server/databases.js new file mode 100644 index 0000000..1a943ce --- /dev/null +++ b/srcs/phpmyadmin/js/server/databases.js @@ -0,0 +1,152 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview functions used on the server databases list page + * @name Server Databases + * + * @requires jQuery + * @requires jQueryUI + * @required js/functions.js + */ + +/* global MicroHistory */ // js/microhistory.js + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/databases.js', function () { + $(document).off('submit', '#dbStatsForm'); + $(document).off('submit', '#create_database_form.ajax'); +}); + +/** + * AJAX scripts for server_databases.php + * + * Actions ajaxified here: + * Drop Databases + * + */ +AJAX.registerOnload('server/databases.js', function () { + /** + * Attach Event Handler for 'Drop Databases' + */ + $(document).on('submit', '#dbStatsForm', function (event) { + event.preventDefault(); + + var $form = $(this); + + /** + * @var selected_dbs Array containing the names of the checked databases + */ + var selectedDbs = []; + // loop over all checked checkboxes, except the .checkall_box checkbox + $form.find('input:checkbox:checked:not(.checkall_box)').each(function () { + $(this).closest('tr').addClass('removeMe'); + selectedDbs[selectedDbs.length] = 'DROP DATABASE `' + Functions.escapeHtml($(this).val()) + '`;'; + }); + if (! selectedDbs.length) { + Functions.ajaxShowMessage( + $('
').text( + Messages.strNoDatabasesSelected + ), + 2000 + ); + return; + } + /** + * @var question String containing the question to be asked for confirmation + */ + var question = Messages.strDropDatabaseStrongWarning + ' ' + + Functions.sprintf(Messages.strDoYouReally, selectedDbs.join('
')); + + var argsep = CommonParams.get('arg_separator'); + $(this).confirm( + question, + $form.prop('action') + '?' + $(this).serialize() + + argsep + 'drop_selected_dbs=1', + function (url) { + Functions.ajaxShowMessage(Messages.strProcessingRequest, false); + + var parts = url.split('?'); + var params = Functions.getJsConfirmCommonParam(this, parts[1]); + + $.post(parts[0], params, function (data) { + if (typeof data !== 'undefined' && data.success === true) { + Functions.ajaxShowMessage(data.message); + + var $rowsToRemove = $form.find('tr.removeMe'); + var $databasesCount = $('#filter-rows-count'); + var newCount = parseInt($databasesCount.text(), 10) - $rowsToRemove.length; + $databasesCount.text(newCount); + + $rowsToRemove.remove(); + $form.find('tbody').sortTable('.name'); + if ($form.find('tbody').find('tr').length === 0) { + // user just dropped the last db on this page + CommonActions.refreshMain(); + } + Navigation.reload(); + } else { + $form.find('tr.removeMe').removeClass('removeMe'); + Functions.ajaxShowMessage(data.error, false); + } + }); // end $.post() + } + ); + }); // end of Drop Database action + + /** + * Attach Ajax event handlers for 'Create Database'. + */ + $(document).on('submit', '#create_database_form.ajax', function (event) { + event.preventDefault(); + + var $form = $(this); + + // TODO Remove this section when all browsers support HTML5 "required" property + var newDbNameInput = $form.find('input[name=new_db]'); + if (newDbNameInput.val() === '') { + newDbNameInput.trigger('focus'); + alert(Messages.strFormEmpty); + return; + } + // end remove + + Functions.ajaxShowMessage(Messages.strProcessingRequest); + Functions.prepareForAjaxRequest($form); + + $.post($form.attr('action'), $form.serialize(), function (data) { + if (typeof data !== 'undefined' && data.success === true) { + Functions.ajaxShowMessage(data.message); + + var $databasesCountObject = $('#filter-rows-count'); + var databasesCount = parseInt($databasesCountObject.text(), 10) + 1; + $databasesCountObject.text(databasesCount); + Navigation.reload(); + + // make ajax request to load db structure page - taken from ajax.js + var dbStructUrl = data.url_query; + dbStructUrl = dbStructUrl.replace(/amp;/ig, ''); + var params = 'ajax_request=true' + CommonParams.get('arg_separator') + 'ajax_page_request=true'; + if (! (history && history.pushState)) { + params += MicroHistory.menus.getRequestParam(); + } + $.get(dbStructUrl, params, AJAX.responseHandler); + } else { + Functions.ajaxShowMessage(data.error, false); + } + }); // end $.post() + }); // end $(document).on() + + /* Don't show filter if number of databases are very few */ + var databasesCount = $('#filter-rows-count').html(); + if (databasesCount <= 10) { + $('#tableFilter').hide(); + } + + var tableRows = $('.server_databases'); + $.each(tableRows, function () { + $(this).on('click', function () { + CommonActions.setDb($(this).attr('data')); + }); + }); +}); // end $() diff --git a/srcs/phpmyadmin/js/server/plugins.js b/srcs/phpmyadmin/js/server/plugins.js new file mode 100644 index 0000000..bb83247 --- /dev/null +++ b/srcs/phpmyadmin/js/server/plugins.js @@ -0,0 +1,16 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Functions used in server plugins pages + */ +AJAX.registerOnload('server/plugins.js', function () { + // Make columns sortable, but only for tables with more than 1 data row + var $tables = $('#plugins_plugins table:has(tbody tr + tr)'); + $tables.tablesorter({ + sortList: [[0, 0]], + headers: { + 1: { sorter: false } + } + }); + $tables.find('thead th') + .append('
'); +}); diff --git a/srcs/phpmyadmin/js/server/privileges.js b/srcs/phpmyadmin/js/server/privileges.js new file mode 100644 index 0000000..d3bac3e --- /dev/null +++ b/srcs/phpmyadmin/js/server/privileges.js @@ -0,0 +1,494 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview functions used in server privilege pages + * @name Server Privileges + * + * @requires jQuery + * @requires jQueryUI + * @requires js/functions.js + * + */ + +/* global checkboxesSel */ // js/functions.js +/* global zxcvbn */ // js/vendor/zxcvbn.js + +/** + * Validates the "add a user" form + * + * @return boolean whether the form is validated or not + */ +// eslint-disable-next-line no-unused-vars +function checkAddUser (theForm) { + if (theForm.elements.pred_hostname.value === 'userdefined' && theForm.elements.hostname.value === '') { + alert(Messages.strHostEmpty); + theForm.elements.hostname.focus(); + return false; + } + + if (theForm.elements.pred_username.value === 'userdefined' && theForm.elements.username.value === '') { + alert(Messages.strUserEmpty); + theForm.elements.username.focus(); + return false; + } + + return Functions.checkPassword($(theForm)); +} // end of the 'checkAddUser()' function + +function checkPasswordStrength (value, meterObject, meterObjectLabel, username) { + // List of words we don't want to appear in the password + var customDict = [ + 'phpmyadmin', + 'mariadb', + 'mysql', + 'php', + 'my', + 'admin', + ]; + if (username !== null) { + customDict.push(username); + } + var zxcvbnObject = zxcvbn(value, customDict); + var strength = zxcvbnObject.score; + strength = parseInt(strength); + meterObject.val(strength); + switch (strength) { + case 0: meterObjectLabel.html(Messages.strExtrWeak); + break; + case 1: meterObjectLabel.html(Messages.strVeryWeak); + break; + case 2: meterObjectLabel.html(Messages.strWeak); + break; + case 3: meterObjectLabel.html(Messages.strGood); + break; + case 4: meterObjectLabel.html(Messages.strStrong); + } +} + +/** + * AJAX scripts for server_privileges page. + * + * Actions ajaxified here: + * Add user + * Revoke a user + * Edit privileges + * Export privileges + * Paginate table of users + * Flush privileges + * + * @memberOf jQuery + * @name document.ready + */ + + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/privileges.js', function () { + $('#fieldset_add_user_login').off('change', 'input[name=\'username\']'); + $(document).off('click', '#fieldset_delete_user_footer #buttonGo.ajax'); + $(document).off('click', 'a.edit_user_group_anchor.ajax'); + $(document).off('click', 'button.mult_submit[value=export]'); + $(document).off('click', 'a.export_user_anchor.ajax'); + $(document).off('click', '#initials_table a.ajax'); + $('#checkbox_drop_users_db').off('click'); + $(document).off('click', '.checkall_box'); + $(document).off('change', '#checkbox_SSL_priv'); + $(document).off('change', 'input[name="ssl_type"]'); + $(document).off('change', '#select_authentication_plugin'); +}); + +AJAX.registerOnload('server/privileges.js', function () { + /** + * Display a warning if there is already a user by the name entered as the username. + */ + $('#fieldset_add_user_login').on('change', 'input[name=\'username\']', function () { + var username = $(this).val(); + var $warning = $('#user_exists_warning'); + if ($('#select_pred_username').val() === 'userdefined' && username !== '') { + var href = $('form[name=\'usersForm\']').attr('action'); + var params = { + 'ajax_request' : true, + 'server' : CommonParams.get('server'), + 'validate_username' : true, + 'username' : username + }; + $.get(href, params, function (data) { + if (data.user_exists) { + $warning.show(); + } else { + $warning.hide(); + } + }); + } else { + $warning.hide(); + } + }); + + /** + * Indicating password strength + */ + $('#text_pma_pw').on('keyup', function () { + var meterObj = $('#password_strength_meter'); + var meterObjLabel = $('#password_strength'); + var username = $('input[name="username"]'); + username = username.val(); + checkPasswordStrength($(this).val(), meterObj, meterObjLabel, username); + }); + + /** + * Automatically switching to 'Use Text field' from 'No password' once start writing in text area + */ + $('#text_pma_pw').on('input', function () { + if ($('#text_pma_pw').val() !== '') { + $('#select_pred_password').val('userdefined'); + } + }); + + $('#text_pma_change_pw').on('keyup', function () { + var meterObj = $('#change_password_strength_meter'); + var meterObjLabel = $('#change_password_strength'); + checkPasswordStrength($(this).val(), meterObj, meterObjLabel, CommonParams.get('user')); + }); + + /** + * Display a notice if sha256_password is selected + */ + $(document).on('change', '#select_authentication_plugin', function () { + var selectedPlugin = $(this).val(); + if (selectedPlugin === 'sha256_password') { + $('#ssl_reqd_warning').show(); + } else { + $('#ssl_reqd_warning').hide(); + } + }); + + /** + * AJAX handler for 'Revoke User' + * + * @see Functions.ajaxShowMessage() + * @memberOf jQuery + * @name revoke_user_click + */ + $(document).on('click', '#fieldset_delete_user_footer #buttonGo.ajax', function (event) { + event.preventDefault(); + + var $thisButton = $(this); + var $form = $('#usersForm'); + + $thisButton.confirm(Messages.strDropUserWarning, $form.attr('action'), function (url) { + var $dropUsersDbCheckbox = $('#checkbox_drop_users_db'); + if ($dropUsersDbCheckbox.is(':checked')) { + var isConfirmed = confirm(Messages.strDropDatabaseStrongWarning + '\n' + Functions.sprintf(Messages.strDoYouReally, 'DROP DATABASE')); + if (! isConfirmed) { + // Uncheck the drop users database checkbox + $dropUsersDbCheckbox.prop('checked', false); + } + } + + Functions.ajaxShowMessage(Messages.strRemovingSelectedUsers); + + var argsep = CommonParams.get('arg_separator'); + $.post(url, $form.serialize() + argsep + 'delete=' + $thisButton.val() + argsep + 'ajax_request=true', function (data) { + if (typeof data !== 'undefined' && data.success === true) { + Functions.ajaxShowMessage(data.message); + // Refresh navigation, if we droppped some databases with the name + // that is the same as the username of the deleted user + if ($('#checkbox_drop_users_db:checked').length) { + Navigation.reload(); + } + // Remove the revoked user from the users list + $form.find('input:checkbox:checked').parents('tr').slideUp('medium', function () { + var thisUserInitial = $(this).find('input:checkbox').val().charAt(0).toUpperCase(); + $(this).remove(); + + // If this is the last user with this_user_initial, remove the link from #initials_table + if ($('#tableuserrights').find('input:checkbox[value^="' + thisUserInitial + '"], input:checkbox[value^="' + thisUserInitial.toLowerCase() + '"]').length === 0) { + $('#initials_table').find('td > a:contains(' + thisUserInitial + ')').parent('td').html(thisUserInitial); + } + + // Re-check the classes of each row + $form + .find('tbody').find('tr:odd') + .removeClass('even').addClass('odd') + .end() + .find('tr:even') + .removeClass('odd').addClass('even'); + + // update the checkall checkbox + $(checkboxesSel).trigger('change'); + }); + } else { + Functions.ajaxShowMessage(data.error, false); + } + }); // end $.post() + }); + }); // end Revoke User + + $(document).on('click', 'a.edit_user_group_anchor.ajax', function (event) { + event.preventDefault(); + $(this).parents('tr').addClass('current_row'); + var $msg = Functions.ajaxShowMessage(); + $.get( + $(this).attr('href'), + { + 'ajax_request': true, + 'edit_user_group_dialog': true + }, + function (data) { + if (typeof data !== 'undefined' && data.success === true) { + Functions.ajaxRemoveMessage($msg); + var buttonOptions = {}; + buttonOptions[Messages.strGo] = function () { + var usrGroup = $('#changeUserGroupDialog') + .find('select[name="userGroup"]') + .val(); + var $message = Functions.ajaxShowMessage(); + var argsep = CommonParams.get('arg_separator'); + $.post( + 'server_privileges.php', + $('#changeUserGroupDialog').find('form').serialize() + argsep + 'ajax_request=1', + function (data) { + Functions.ajaxRemoveMessage($message); + if (typeof data !== 'undefined' && data.success === true) { + $('#usersForm') + .find('.current_row') + .removeClass('current_row') + .find('.usrGroup') + .text(usrGroup); + } else { + Functions.ajaxShowMessage(data.error, false); + $('#usersForm') + .find('.current_row') + .removeClass('current_row'); + } + } + ); + $(this).dialog('close'); + }; + buttonOptions[Messages.strClose] = function () { + $(this).dialog('close'); + }; + var $dialog = $('
') + .attr('id', 'changeUserGroupDialog') + .append(data.message) + .dialog({ + width: 500, + minWidth: 300, + modal: true, + buttons: buttonOptions, + title: $('legend', $(data.message)).text(), + close: function () { + $(this).remove(); + } + }); + $dialog.find('legend').remove(); + } else { + Functions.ajaxShowMessage(data.error, false); + $('#usersForm') + .find('.current_row') + .removeClass('current_row'); + } + } + ); + }); + + /** + * AJAX handler for 'Export Privileges' + * + * @see Functions.ajaxShowMessage() + * @memberOf jQuery + * @name export_user_click + */ + $(document).on('click', 'button.mult_submit[value=export]', function (event) { + event.preventDefault(); + // can't export if no users checked + if ($(this.form).find('input:checked').length === 0) { + Functions.ajaxShowMessage(Messages.strNoAccountSelected, 2000, 'success'); + return; + } + var $msgbox = Functions.ajaxShowMessage(); + var buttonOptions = {}; + buttonOptions[Messages.strClose] = function () { + $(this).dialog('close'); + }; + var argsep = CommonParams.get('arg_separator'); + var serverId = CommonParams.get('server'); + var selectedUsers = $('#usersForm input[name*=\'selected_usr\']:checkbox').serialize(); + var postStr = selectedUsers + '&submit_mult=export' + argsep + 'ajax_request=true&server=' + serverId; + $.post( + $(this.form).prop('action'), + postStr, + function (data) { + if (typeof data !== 'undefined' && data.success === true) { + var $ajaxDialog = $('
') + .append(data.message) + .dialog({ + title: data.title, + width: 500, + buttons: buttonOptions, + close: function () { + $(this).remove(); + } + }); + Functions.ajaxRemoveMessage($msgbox); + // Attach syntax highlighted editor to export dialog + Functions.getSqlEditor($ajaxDialog.find('textarea')); + } else { + Functions.ajaxShowMessage(data.error, false); + } + } + ); // end $.post + }); + // if exporting non-ajax, highlight anyways + Functions.getSqlEditor($('textarea.export')); + + $(document).on('click', 'a.export_user_anchor.ajax', function (event) { + event.preventDefault(); + var $msgbox = Functions.ajaxShowMessage(); + /** + * @var button_options Object containing options for jQueryUI dialog buttons + */ + var buttonOptions = {}; + buttonOptions[Messages.strClose] = function () { + $(this).dialog('close'); + }; + $.get($(this).attr('href'), { 'ajax_request': true }, function (data) { + if (typeof data !== 'undefined' && data.success === true) { + var $ajaxDialog = $('
') + .append(data.message) + .dialog({ + title: data.title, + width: 500, + buttons: buttonOptions, + close: function () { + $(this).remove(); + } + }); + Functions.ajaxRemoveMessage($msgbox); + // Attach syntax highlighted editor to export dialog + Functions.getSqlEditor($ajaxDialog.find('textarea')); + } else { + Functions.ajaxShowMessage(data.error, false); + } + }); // end $.get + }); // end export privileges + + /** + * AJAX handler to Paginate the Users Table + * + * @see Functions.ajaxShowMessage() + * @name paginate_users_table_click + * @memberOf jQuery + */ + $(document).on('click', '#initials_table a.ajax', function (event) { + event.preventDefault(); + var $msgbox = Functions.ajaxShowMessage(); + $.get($(this).attr('href'), { 'ajax_request' : true }, function (data) { + if (typeof data !== 'undefined' && data.success === true) { + Functions.ajaxRemoveMessage($msgbox); + // This form is not on screen when first entering Privileges + // if there are more than 50 users + $('div.notice').remove(); + $('#usersForm').hide('medium').remove(); + $('#fieldset_add_user').hide('medium').remove(); + $('#initials_table') + .prop('id', 'initials_table_old') + .after(data.message).show('medium') + .siblings('h2').not(':first').remove(); + // prevent double initials table + $('#initials_table_old').remove(); + } else { + Functions.ajaxShowMessage(data.error, false); + } + }); // end $.get + }); // end of the paginate users table + + $(document).on('change', 'input[name="ssl_type"]', function () { + var $div = $('#specified_div'); + if ($('#ssl_type_SPECIFIED').is(':checked')) { + $div.find('input').prop('disabled', false); + } else { + $div.find('input').prop('disabled', true); + } + }); + + $(document).on('change', '#checkbox_SSL_priv', function () { + var $div = $('#require_ssl_div'); + if ($(this).is(':checked')) { + $div.find('input').prop('disabled', false); + $('#ssl_type_SPECIFIED').trigger('change'); + } else { + $div.find('input').prop('disabled', true); + } + }); + + $('#checkbox_SSL_priv').trigger('change'); + + /* + * Create submenu for simpler interface + */ + var addOrUpdateSubmenu = function () { + var $topmenu2 = $('#topmenu2'); + var $editUserDialog = $('#edit_user_dialog'); + var submenuLabel; + var submenuLink; + var linkNumber; + + // if submenu exists yet, remove it first + if ($topmenu2.length > 0) { + $topmenu2.remove(); + } + + // construct a submenu from the existing fieldsets + $topmenu2 = $('').prop('id', 'topmenu2'); + + $('#edit_user_dialog .submenu-item').each(function () { + submenuLabel = $(this).find('legend[data-submenu-label]').data('submenu-label'); + + submenuLink = $('') + .prop('href', '#') + .html(submenuLabel); + + $('
  • ') + .append(submenuLink) + .appendTo($topmenu2); + }); + + // click handlers for submenu + $topmenu2.find('a').on('click', function (e) { + e.preventDefault(); + // if already active, ignore click + if ($(this).hasClass('tabactive')) { + return; + } + $topmenu2.find('a').removeClass('tabactive'); + $(this).addClass('tabactive'); + + // which section to show now? + linkNumber = $topmenu2.find('a').index($(this)); + // hide all sections but the one to show + $('#edit_user_dialog .submenu-item').hide().eq(linkNumber).show(); + }); + + // make first menu item active + // TODO: support URL hash history + $topmenu2.find('> :first-child a').addClass('tabactive'); + $editUserDialog.prepend($topmenu2); + + // hide all sections but the first + $('#edit_user_dialog .submenu-item').hide().eq(0).show(); + + // scroll to the top + $('html, body').animate({ scrollTop: 0 }, 'fast'); + }; + + $('input.autofocus').trigger('focus'); + $(checkboxesSel).trigger('change'); + Functions.displayPasswordGenerateButton(); + if ($('#edit_user_dialog').length > 0) { + addOrUpdateSubmenu(); + } + + var windowWidth = $(window).width(); + $('.jsresponsive').css('max-width', (windowWidth - 35) + 'px'); +}); diff --git a/srcs/phpmyadmin/js/server/status/advisor.js b/srcs/phpmyadmin/js/server/status/advisor.js new file mode 100644 index 0000000..8fe43cf --- /dev/null +++ b/srcs/phpmyadmin/js/server/status/advisor.js @@ -0,0 +1,100 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Server Status Advisor + * + * @package PhpMyAdmin + */ + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/status/advisor.js', function () { + $('a[href="#openAdvisorInstructions"]').off('click'); + $('#statustabs_advisor').html(''); + $('#advisorDialog').remove(); + $('#instructionsDialog').remove(); +}); + +AJAX.registerOnload('server/status/advisor.js', function () { + // if no advisor is loaded + if ($('#advisorData').length === 0) { + return; + } + + /** ** Server config advisor ****/ + var $dialog = $('
    ').attr('id', 'advisorDialog'); + var $instructionsDialog = $('
    ') + .attr('id', 'instructionsDialog') + .html($('#advisorInstructionsDialog').html()); + + $('a[href="#openAdvisorInstructions"]').on('click', function () { + var dlgBtns = {}; + dlgBtns[Messages.strClose] = function () { + $(this).dialog('close'); + }; + $instructionsDialog.dialog({ + title: Messages.strAdvisorSystem, + width: '60%', + buttons: dlgBtns + }); + }); + + var $cnt = $('#statustabs_advisor'); + var $tbody; + var $tr; + var even = true; + + var data = JSON.parse($('#advisorData').text()); + $cnt.html(''); + + if (data.parse.errors.length > 0) { + $cnt.append('Rules file not well formed, following errors were found:
    - '); + $cnt.append(data.parse.errors.join('
    - ')); + $cnt.append('

    '); + } + + if (data.run.errors.length > 0) { + $cnt.append('Errors occurred while executing rule expressions:
    - '); + $cnt.append(data.run.errors.join('
    - ')); + $cnt.append('

    '); + } + + if (data.run.fired.length > 0) { + $cnt.append('

    ' + Messages.strPerformanceIssues + '

    '); + $cnt.append('' + + '
    ' + Messages.strIssuse + '' + Messages.strRecommendation + + '
    '); + $tbody = $cnt.find('table#rulesFired'); + + var rcStripped; + + $.each(data.run.fired, function (key, value) { + // recommendation may contain links, don't show those in overview table (clicking on them redirects the user) + rcStripped = $.trim($('
    ').html(value.recommendation).text()); + $tbody.append($tr = $('' + + value.issue + '' + rcStripped + ' ')); + even = !even; + $tr.data('rule', value); + + $tr.on('click', function () { + var rule = $(this).data('rule'); + $dialog + .dialog({ title: Messages.strRuleDetails }) + .html( + '

    ' + Messages.strIssuse + ':
    ' + rule.issue + '

    ' + + '

    ' + Messages.strRecommendation + ':
    ' + rule.recommendation + '

    ' + + '

    ' + Messages.strJustification + ':
    ' + rule.justification + '

    ' + + '

    ' + Messages.strFormula + ':
    ' + rule.formula + '

    ' + + '

    ' + Messages.strTest + ':
    ' + rule.test + '

    ' + ); + + var dlgBtns = {}; + dlgBtns[Messages.strClose] = function () { + $(this).dialog('close'); + }; + + $dialog.dialog({ width: 600, buttons: dlgBtns }); + }); + }); + } +}); diff --git a/srcs/phpmyadmin/js/server/status/monitor.js b/srcs/phpmyadmin/js/server/status/monitor.js new file mode 100644 index 0000000..3cb4fe9 --- /dev/null +++ b/srcs/phpmyadmin/js/server/status/monitor.js @@ -0,0 +1,2219 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview Javascript functions used in server status monitor page + * @name Server Status Monitor + * + * @requires jQuery + * @requires jQueryUI + * @requires js/functions.js + */ + +/* global isStorageSupported */ // js/config.js +/* global codeMirrorEditor:writable */ // js/functions.js +/* global pmaThemeImage */ // js/messages.php +/* global variableNames */ // templates/server/status/monitor/index.twig + +var runtime = {}; +var serverTimeDiff; +var serverOs; +var isSuperUser; +var serverDbIsLocal; +var chartSize; +var monitorSettings; + +AJAX.registerOnload('server/status/monitor.js', function () { + var $jsDataForm = $('#js_data'); + serverTimeDiff = new Date().getTime() - $jsDataForm.find('input[name=server_time]').val(); + serverOs = $jsDataForm.find('input[name=server_os]').val(); + isSuperUser = $jsDataForm.find('input[name=is_superuser]').val(); + serverDbIsLocal = $jsDataForm.find('input[name=server_db_isLocal]').val(); +}); + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/status/monitor.js', function () { + $('#emptyDialog').remove(); + $('#addChartDialog').remove(); + $('a.popupLink').off('click'); + $('body').off('click'); +}); +/** + * Popup behaviour + */ +AJAX.registerOnload('server/status/monitor.js', function () { + $('
    ') + .attr('id', 'emptyDialog') + .appendTo('#page_content'); + $('#addChartDialog') + .appendTo('#page_content'); + + $('a.popupLink').on('click', function () { + var $link = $(this); + $('div.' + $link.attr('href').substr(1)) + .show() + .offset({ top: $link.offset().top + $link.height() + 5, left: $link.offset().left }) + .addClass('openedPopup'); + + return false; + }); + $('body').on('click', function (event) { + $('div.openedPopup').each(function () { + var $cnt = $(this); + var pos = $cnt.offset(); + // Hide if the mouseclick is outside the popupcontent + if (event.pageX < pos.left || + event.pageY < pos.top || + event.pageX > pos.left + $cnt.outerWidth() || + event.pageY > pos.top + $cnt.outerHeight() + ) { + $cnt.hide().removeClass('openedPopup'); + } + }); + }); +}); + +AJAX.registerTeardown('server/status/monitor.js', function () { + $('a[href="#rearrangeCharts"], a[href="#endChartEditMode"]').off('click'); + $('div.popupContent select[name="chartColumns"]').off('change'); + $('div.popupContent select[name="gridChartRefresh"]').off('change'); + $('a[href="#addNewChart"]').off('click'); + $('a[href="#exportMonitorConfig"]').off('click'); + $('a[href="#importMonitorConfig"]').off('click'); + $('a[href="#clearMonitorConfig"]').off('click'); + $('a[href="#pauseCharts"]').off('click'); + $('a[href="#monitorInstructionsDialog"]').off('click'); + $('input[name="chartType"]').off('click'); + $('input[name="useDivisor"]').off('click'); + $('input[name="useUnit"]').off('click'); + $('select[name="varChartList"]').off('click'); + $('a[href="#kibDivisor"]').off('click'); + $('a[href="#mibDivisor"]').off('click'); + $('a[href="#submitClearSeries"]').off('click'); + $('a[href="#submitAddSeries"]').off('click'); + // $("input#variableInput").destroy(); + $('#chartPreset').off('click'); + $('#chartStatusVar').off('click'); + destroyGrid(); +}); + +AJAX.registerOnload('server/status/monitor.js', function () { + // Show tab links + $('div.tabLinks').show(); + $('#loadingMonitorIcon').remove(); + // Codemirror is loaded on demand so we might need to initialize it + if (! codeMirrorEditor) { + var $elm = $('#sqlquery'); + if ($elm.length > 0 && typeof CodeMirror !== 'undefined') { + codeMirrorEditor = CodeMirror.fromTextArea( + $elm[0], + { + lineNumbers: true, + matchBrackets: true, + indentUnit: 4, + mode: 'text/x-mysql', + lineWrapping: true + } + ); + } + } + // Timepicker is loaded on demand so we need to initialize + // datetime fields from the 'load log' dialog + $('#logAnalyseDialog').find('.datetimefield').each(function () { + Functions.addDatepicker($(this)); + }); + + /** ** Monitor charting implementation ****/ + /* Saves the previous ajax response for differential values */ + var oldChartData = null; + // Holds about to be created chart + var newChart = null; + var chartSpacing; + + // Whenever the monitor object (runtime.charts) or the settings object + // (monitorSettings) changes in a way incompatible to the previous version, + // increase this number. It will reset the users monitor and settings object + // in his localStorage to the default configuration + var monitorProtocolVersion = '1.0'; + + // Runtime parameter of the monitor, is being fully set in initGrid() + runtime = { + // Holds all visible charts in the grid + charts: null, + // Stores the timeout handler so it can be cleared + refreshTimeout: null, + // Stores the GET request to refresh the charts + refreshRequest: null, + // Chart auto increment + chartAI: 0, + // To play/pause the monitor + redrawCharts: false, + // Object that contains a list of nodes that need to be retrieved + // from the server for chart updates + dataList: [], + // Current max points per chart (needed for auto calculation) + gridMaxPoints: 20, + // displayed time frame + xmin: -1, + xmax: -1 + }; + monitorSettings = null; + + var defaultMonitorSettings = { + columns: 3, + chartSize: { width: 295, height: 250 }, + // Max points in each chart. Settings it to 'auto' sets + // gridMaxPoints to (chartwidth - 40) / 12 + gridMaxPoints: 'auto', + /* Refresh rate of all grid charts in ms */ + gridRefresh: 5000 + }; + + // Allows drag and drop rearrange and print/edit icons on charts + var editMode = false; + + /* List of preconfigured charts that the user may select */ + var presetCharts = { + // Query cache efficiency + 'qce': { + title: Messages.strQueryCacheEfficiency, + series: [{ + label: Messages.strQueryCacheEfficiency + }], + nodes: [{ + dataPoints: [{ type: 'statusvar', name: 'Qcache_hits' }, { type: 'statusvar', name: 'Com_select' }], + transformFn: 'qce' + }], + maxYLabel: 0 + }, + // Query cache usage + 'qcu': { + title: Messages.strQueryCacheUsage, + series: [{ + label: Messages.strQueryCacheUsed + }], + nodes: [{ + dataPoints: [{ type: 'statusvar', name: 'Qcache_free_memory' }, { type: 'servervar', name: 'query_cache_size' }], + transformFn: 'qcu' + }], + maxYLabel: 0 + } + }; + + // time span selection + var selectionTimeDiff = []; + var selectionStartX; + var selectionStartY; + var drawTimeSpan = false; + + /* Add OS specific system info charts to the preset chart list */ + switch (serverOs) { + case 'WINNT': + $.extend(presetCharts, { + 'cpu': { + title: Messages.strSystemCPUUsage, + series: [{ + label: Messages.strAverageLoad + }], + nodes: [{ + dataPoints: [{ type: 'cpu', name: 'loadavg' }] + }], + maxYLabel: 100 + }, + + 'memory': { + title: Messages.strSystemMemory, + series: [{ + label: Messages.strTotalMemory, + fill: true + }, { + dataType: 'memory', + label: Messages.strUsedMemory, + fill: true + }], + nodes: [{ dataPoints: [{ type: 'memory', name: 'MemTotal' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + }, + + 'swap': { + title: Messages.strSystemSwap, + series: [{ + label: Messages.strTotalSwap, + fill: true + }, { + label: Messages.strUsedSwap, + fill: true + }], + nodes: [{ dataPoints: [{ type: 'memory', name: 'SwapTotal' }] }, + { dataPoints: [{ type: 'memory', name: 'SwapUsed' }] } + ], + maxYLabel: 0 + } + }); + break; + + case 'Linux': + $.extend(presetCharts, { + 'cpu': { + title: Messages.strSystemCPUUsage, + series: [{ + label: Messages.strAverageLoad + }], + nodes: [{ dataPoints: [{ type: 'cpu', name: 'irrelevant' }], transformFn: 'cpu-linux' }], + maxYLabel: 0 + }, + 'memory': { + title: Messages.strSystemMemory, + series: [ + { label: Messages.strBufferedMemory, fill: true }, + { label: Messages.strUsedMemory, fill: true }, + { label: Messages.strCachedMemory, fill: true }, + { label: Messages.strFreeMemory, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'Buffers' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'Cached' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + }, + 'swap': { + title: Messages.strSystemSwap, + series: [ + { label: Messages.strCachedSwap, fill: true }, + { label: Messages.strUsedSwap, fill: true }, + { label: Messages.strFreeSwap, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'SwapCached' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'SwapUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'SwapFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + } + }); + break; + + case 'SunOS': + $.extend(presetCharts, { + 'cpu': { + title: Messages.strSystemCPUUsage, + series: [{ + label: Messages.strAverageLoad + }], + nodes: [{ + dataPoints: [{ type: 'cpu', name: 'loadavg' }] + }], + maxYLabel: 0 + }, + 'memory': { + title: Messages.strSystemMemory, + series: [ + { label: Messages.strUsedMemory, fill: true }, + { label: Messages.strFreeMemory, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + }, + 'swap': { + title: Messages.strSystemSwap, + series: [ + { label: Messages.strUsedSwap, fill: true }, + { label: Messages.strFreeSwap, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'SwapUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'SwapFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + } + }); + break; + } + + // Default setting for the chart grid + var defaultChartGrid = { + 'c0': { + title: Messages.strQuestions, + series: [ + { label: Messages.strQuestions } + ], + nodes: [ + { dataPoints: [{ type: 'statusvar', name: 'Questions' }], display: 'differential' } + ], + maxYLabel: 0 + }, + 'c1': { + title: Messages.strChartConnectionsTitle, + series: [ + { label: Messages.strConnections }, + { label: Messages.strProcesses } + ], + nodes: [ + { dataPoints: [{ type: 'statusvar', name: 'Connections' }], display: 'differential' }, + { dataPoints: [{ type: 'proc', name: 'processes' }] } + ], + maxYLabel: 0 + }, + 'c2': { + title: Messages.strTraffic, + series: [ + { label: Messages.strBytesSent }, + { label: Messages.strBytesReceived } + ], + nodes: [ + { dataPoints: [{ type: 'statusvar', name: 'Bytes_sent' }], display: 'differential', valueDivisor: 1024 }, + { dataPoints: [{ type: 'statusvar', name: 'Bytes_received' }], display: 'differential', valueDivisor: 1024 } + ], + maxYLabel: 0 + } + }; + + // Server is localhost => We can add cpu/memory/swap to the default chart + if (serverDbIsLocal && typeof presetCharts.cpu !== 'undefined') { + defaultChartGrid.c3 = presetCharts.cpu; + defaultChartGrid.c4 = presetCharts.memory; + defaultChartGrid.c5 = presetCharts.swap; + } + + $('a[href="#rearrangeCharts"], a[href="#endChartEditMode"]').on('click', function (event) { + event.preventDefault(); + editMode = !editMode; + if ($(this).attr('href') === '#endChartEditMode') { + editMode = false; + } + + $('a[href="#endChartEditMode"]').toggle(editMode); + + if (editMode) { + // Close the settings popup + $('div.popupContent').hide().removeClass('openedPopup'); + + $('#chartGrid').sortableTable({ + ignoreRect: { + top: 8, + left: chartSize.width - 63, + width: 54, + height: 24 + } + }); + } else { + $('#chartGrid').sortableTable('destroy'); + } + saveMonitor(); // Save settings + return false; + }); + + // global settings + $('div.popupContent select[name="chartColumns"]').on('change', function () { + monitorSettings.columns = parseInt(this.value, 10); + + calculateChartSize(); + // Empty cells should keep their size so you can drop onto them + $('#chartGrid').find('tr td').css('width', chartSize.width + 'px'); + $('#chartGrid').find('.monitorChart').css({ + width: chartSize.width + 'px', + height: chartSize.height + 'px' + }); + + /* Reorder all charts that it fills all column cells */ + var numColumns; + var $tr = $('#chartGrid').find('tr:first'); + + var tempManageCols = function () { + if (numColumns > monitorSettings.columns) { + if ($tr.next().length === 0) { + $tr.after(''); + } + $tr.next().prepend($(this)); + } + numColumns++; + }; + + var tempAddCol = function () { + if ($(this).next().length !== 0) { + $(this).append($(this).next().find('td:first')); + } + }; + + while ($tr.length !== 0) { + numColumns = 1; + // To many cells in one row => put into next row + $tr.find('td').each(tempManageCols); + + // To little cells in one row => for each cell to little, + // move all cells backwards by 1 + if ($tr.next().length > 0) { + var cnt = monitorSettings.columns - $tr.find('td').length; + for (var i = 0; i < cnt; i++) { + $tr.append($tr.next().find('td:first')); + $tr.nextAll().each(tempAddCol); + } + } + + $tr = $tr.next(); + } + + if (monitorSettings.gridMaxPoints === 'auto') { + runtime.gridMaxPoints = Math.round((chartSize.width - 40) / 12); + } + + runtime.xmin = new Date().getTime() - serverTimeDiff - runtime.gridMaxPoints * monitorSettings.gridRefresh; + runtime.xmax = new Date().getTime() - serverTimeDiff + monitorSettings.gridRefresh; + + if (editMode) { + $('#chartGrid').sortableTable('refresh'); + } + + refreshChartGrid(); + saveMonitor(); // Save settings + }); + + $('div.popupContent select[name="gridChartRefresh"]').on('change', function () { + monitorSettings.gridRefresh = parseInt(this.value, 10) * 1000; + clearTimeout(runtime.refreshTimeout); + + if (runtime.refreshRequest) { + runtime.refreshRequest.abort(); + } + + runtime.xmin = new Date().getTime() - serverTimeDiff - runtime.gridMaxPoints * monitorSettings.gridRefresh; + // fixing chart shift towards left on refresh rate change + // runtime.xmax = new Date().getTime() - serverTimeDiff + monitorSettings.gridRefresh; + runtime.refreshTimeout = setTimeout(refreshChartGrid, monitorSettings.gridRefresh); + + saveMonitor(); // Save settings + }); + + $('a[href="#addNewChart"]').on('click', function (event) { + event.preventDefault(); + var dlgButtons = { }; + + dlgButtons[Messages.strAddChart] = function () { + var type = $('input[name="chartType"]:checked').val(); + + if (type === 'preset') { + newChart = presetCharts[$('#addChartDialog').find('select[name="presetCharts"]').prop('value')]; + } else { + // If user builds his own chart, it's being set/updated + // each time he adds a series + // So here we only warn if he didn't add a series yet + if (! newChart || ! newChart.nodes || newChart.nodes.length === 0) { + alert(Messages.strAddOneSeriesWarning); + return; + } + } + + newChart.title = $('input[name="chartTitle"]').val(); + // Add a cloned object to the chart grid + addChart($.extend(true, {}, newChart)); + + newChart = null; + + saveMonitor(); // Save settings + + $(this).dialog('close'); + }; + + dlgButtons[Messages.strClose] = function () { + newChart = null; + $('span#clearSeriesLink').hide(); + $('#seriesPreview').html(''); + $(this).dialog('close'); + }; + + var $presetList = $('#addChartDialog').find('select[name="presetCharts"]'); + if ($presetList.html().length === 0) { + $.each(presetCharts, function (key, value) { + $presetList.append(''); + }); + $presetList.on('change', function () { + $('input[name="chartTitle"]').val( + $presetList.find(':selected').text() + ); + $('#chartPreset').prop('checked', true); + }); + $('#chartPreset').on('click', function () { + $('input[name="chartTitle"]').val( + $presetList.find(':selected').text() + ); + }); + $('#chartStatusVar').on('click', function () { + $('input[name="chartTitle"]').val( + $('#chartSeries').find(':selected').text().replace(/_/g, ' ') + ); + }); + $('#chartSeries').on('change', function () { + $('input[name="chartTitle"]').val( + $('#chartSeries').find(':selected').text().replace(/_/g, ' ') + ); + }); + } + + $('#addChartDialog').dialog({ + width: 'auto', + height: 'auto', + buttons: dlgButtons + }); + + $('#seriesPreview').html('' + Messages.strNone + ''); + + return false; + }); + + $('a[href="#exportMonitorConfig"]').on('click', function (event) { + event.preventDefault(); + var gridCopy = {}; + $.each(runtime.charts, function (key, elem) { + gridCopy[key] = {}; + gridCopy[key].nodes = elem.nodes; + gridCopy[key].series = elem.series; + gridCopy[key].settings = elem.settings; + gridCopy[key].title = elem.title; + gridCopy[key].maxYLabel = elem.maxYLabel; + }); + var exportData = { + monitorCharts: gridCopy, + monitorSettings: monitorSettings + }; + + var blob = new Blob([JSON.stringify(exportData)], { type: 'application/octet-stream' }); + var url = null; + var fileName = 'monitor-config.json'; + if (window.navigator && window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveOrOpenBlob(blob, fileName); + } else { + url = URL.createObjectURL(blob); + window.location.href = url; + } + setTimeout(function () { + // For some browsers it is necessary to delay revoking the ObjectURL + if (url !== null) { + window.URL.revokeObjectURL(url); + } + url = undefined; + blob = undefined; + }, 100); + }); + + $('a[href="#importMonitorConfig"]').on('click', function (event) { + event.preventDefault(); + $('#emptyDialog').dialog({ title: Messages.strImportDialogTitle }); + $('#emptyDialog').html(Messages.strImportDialogMessage + ':
    ' + + '
    '); + + var dlgBtns = {}; + + dlgBtns[Messages.strImport] = function () { + var input = $('#emptyDialog').find('#import_file')[0]; + var reader = new FileReader(); + + reader.onerror = function (event) { + alert(Messages.strFailedParsingConfig + '\n' + event.target.error.code); + }; + reader.onload = function (e) { + var data = e.target.result; + var json = null; + // Try loading config + try { + json = JSON.parse(data); + } catch (err) { + alert(Messages.strFailedParsingConfig); + $('#emptyDialog').dialog('close'); + return; + } + + // Basic check, is this a monitor config json? + if (!json || ! json.monitorCharts || ! json.monitorCharts) { + alert(Messages.strFailedParsingConfig); + $('#emptyDialog').dialog('close'); + return; + } + + // If json ok, try applying config + try { + if (isStorageSupported('localStorage')) { + window.localStorage.monitorCharts = JSON.stringify(json.monitorCharts); + window.localStorage.monitorSettings = JSON.stringify(json.monitorSettings); + } + rebuildGrid(); + } catch (err) { + alert(Messages.strFailedBuildingGrid); + // If an exception is thrown, load default again + if (isStorageSupported('localStorage')) { + window.localStorage.removeItem('monitorCharts'); + window.localStorage.removeItem('monitorSettings'); + } + rebuildGrid(); + } + + $('#emptyDialog').dialog('close'); + }; + reader.readAsText(input.files[0]); + }; + + dlgBtns[Messages.strCancel] = function () { + $(this).dialog('close'); + }; + + $('#emptyDialog').dialog({ + width: 'auto', + height: 'auto', + buttons: dlgBtns + }); + }); + + $('a[href="#clearMonitorConfig"]').on('click', function (event) { + event.preventDefault(); + if (isStorageSupported('localStorage')) { + window.localStorage.removeItem('monitorCharts'); + window.localStorage.removeItem('monitorSettings'); + window.localStorage.removeItem('monitorVersion'); + } + $(this).hide(); + rebuildGrid(); + }); + + $('a[href="#pauseCharts"]').on('click', function (event) { + event.preventDefault(); + runtime.redrawCharts = ! runtime.redrawCharts; + if (! runtime.redrawCharts) { + $(this).html(Functions.getImage('play') + Messages.strResumeMonitor); + } else { + $(this).html(Functions.getImage('pause') + Messages.strPauseMonitor); + if (! runtime.charts) { + initGrid(); + $('a[href="#settingsPopup"]').show(); + } + } + return false; + }); + + $('a[href="#monitorInstructionsDialog"]').on('click', function (event) { + event.preventDefault(); + + var $dialog = $('#monitorInstructionsDialog'); + var dlgBtns = {}; + dlgBtns[Messages.strClose] = function () { + $(this).dialog('close'); + }; + $dialog.dialog({ + width: '60%', + height: 'auto', + buttons: dlgBtns + }).find('img.ajaxIcon').show(); + + var loadLogVars = function (getvars) { + var vars = { + 'ajax_request': true, + 'logging_vars': true + }; + if (getvars) { + $.extend(vars, getvars); + } + + $.post('server_status_monitor.php' + CommonParams.get('common_query'), vars, + function (data) { + var logVars; + if (typeof data !== 'undefined' && data.success === true) { + logVars = data.message; + } else { + return serverResponseError(); + } + var icon = Functions.getImage('s_success'); + var msg = ''; + var str = ''; + + if (logVars.general_log === 'ON') { + if (logVars.slow_query_log === 'ON') { + msg = Messages.strBothLogOn; + } else { + msg = Messages.strGenLogOn; + } + } + + if (msg.length === 0 && logVars.slow_query_log === 'ON') { + msg = Messages.strSlowLogOn; + } + + if (msg.length === 0) { + icon = Functions.getImage('s_error'); + msg = Messages.strBothLogOff; + } + + str = '' + Messages.strCurrentSettings + '
    '; + str += icon + msg + '
    '; + + if (logVars.log_output !== 'TABLE') { + str += Functions.getImage('s_error') + ' ' + Messages.strLogOutNotTable + '
    '; + } else { + str += Functions.getImage('s_success') + ' ' + Messages.strLogOutIsTable + '
    '; + } + + if (logVars.slow_query_log === 'ON') { + if (logVars.long_query_time > 2) { + str += Functions.getImage('s_attention') + ' '; + str += Functions.sprintf(Messages.strSmallerLongQueryTimeAdvice, logVars.long_query_time); + str += '
    '; + } + + if (logVars.long_query_time < 2) { + str += Functions.getImage('s_success') + ' '; + str += Functions.sprintf(Messages.strLongQueryTimeSet, logVars.long_query_time); + str += '
    '; + } + } + + str += '
    '; + + if (isSuperUser) { + str += '

    ' + Messages.strChangeSettings + ''; + str += '
    '; + str += Messages.strSettingsAppliedGlobal + '
    '; + + var varValue = 'TABLE'; + if (logVars.log_output === 'TABLE') { + varValue = 'FILE'; + } + + str += '- '; + str += Functions.sprintf(Messages.strSetLogOutput, varValue); + str += '
    '; + + if (logVars.general_log !== 'ON') { + str += '- '; + str += Functions.sprintf(Messages.strEnableVar, 'general_log'); + str += '
    '; + } else { + str += '- '; + str += Functions.sprintf(Messages.strDisableVar, 'general_log'); + str += '
    '; + } + + if (logVars.slow_query_log !== 'ON') { + str += '- '; + str += Functions.sprintf(Messages.strEnableVar, 'slow_query_log'); + str += '
    '; + } else { + str += '- '; + str += Functions.sprintf(Messages.strDisableVar, 'slow_query_log'); + str += '
    '; + } + + varValue = 5; + if (logVars.long_query_time > 2) { + varValue = 1; + } + + str += '- '; + str += Functions.sprintf(Messages.setSetLongQueryTime, varValue); + str += '
    '; + } else { + str += Messages.strNoSuperUser + '
    '; + } + + str += '
    '; + + $dialog.find('div.monitorUse').toggle( + logVars.log_output === 'TABLE' && (logVars.slow_query_log === 'ON' || logVars.general_log === 'ON') + ); + + $dialog.find('div.ajaxContent').html(str); + $dialog.find('img.ajaxIcon').hide(); + $dialog.find('a.set').on('click', function () { + var nameValue = $(this).attr('href').split('-'); + loadLogVars({ varName: nameValue[0].substr(1), varValue: nameValue[1] }); + $dialog.find('img.ajaxIcon').show(); + }); + } + ); + }; + + + loadLogVars(); + + return false; + }); + + $('input[name="chartType"]').on('change', function () { + $('#chartVariableSettings').toggle(this.checked && this.value === 'variable'); + var title = $('input[name="chartTitle"]').val(); + if (title === Messages.strChartTitle || + title === $('label[for="' + $('input[name="chartTitle"]').data('lastRadio') + '"]').text() + ) { + $('input[name="chartTitle"]') + .data('lastRadio', $(this).attr('id')) + .val($('label[for="' + $(this).attr('id') + '"]').text()); + } + }); + + $('input[name="useDivisor"]').on('change', function () { + $('span.divisorInput').toggle(this.checked); + }); + + $('input[name="useUnit"]').on('change', function () { + $('span.unitInput').toggle(this.checked); + }); + + $('select[name="varChartList"]').on('change', function () { + if (this.selectedIndex !== 0) { + $('#variableInput').val(this.value); + } + }); + + $('a[href="#kibDivisor"]').on('click', function (event) { + event.preventDefault(); + $('input[name="valueDivisor"]').val(1024); + $('input[name="valueUnit"]').val(Messages.strKiB); + $('span.unitInput').toggle(true); + $('input[name="useUnit"]').prop('checked', true); + return false; + }); + + $('a[href="#mibDivisor"]').on('click', function (event) { + event.preventDefault(); + $('input[name="valueDivisor"]').val(1024 * 1024); + $('input[name="valueUnit"]').val(Messages.strMiB); + $('span.unitInput').toggle(true); + $('input[name="useUnit"]').prop('checked', true); + return false; + }); + + $('a[href="#submitClearSeries"]').on('click', function (event) { + event.preventDefault(); + $('#seriesPreview').html('' + Messages.strNone + ''); + newChart = null; + $('#clearSeriesLink').hide(); + }); + + $('a[href="#submitAddSeries"]').on('click', function (event) { + event.preventDefault(); + if ($('#variableInput').val() === '') { + return false; + } + + if (newChart === null) { + $('#seriesPreview').html(''); + + newChart = { + title: $('input[name="chartTitle"]').val(), + nodes: [], + series: [], + maxYLabel: 0 + }; + } + + var serie = { + dataPoints: [{ type: 'statusvar', name: $('#variableInput').val() }], + display: $('input[name="differentialValue"]').prop('checked') ? 'differential' : '' + }; + + if (serie.dataPoints[0].name === 'Processes') { + serie.dataPoints[0].type = 'proc'; + } + + if ($('input[name="useDivisor"]').prop('checked')) { + serie.valueDivisor = parseInt($('input[name="valueDivisor"]').val(), 10); + } + + if ($('input[name="useUnit"]').prop('checked')) { + serie.unit = $('input[name="valueUnit"]').val(); + } + + var str = serie.display === 'differential' ? ', ' + Messages.strDifferential : ''; + str += serie.valueDivisor ? (', ' + Functions.sprintf(Messages.strDividedBy, serie.valueDivisor)) : ''; + str += serie.unit ? (', ' + Messages.strUnit + ': ' + serie.unit) : ''; + + var newSeries = { + label: $('#variableInput').val().replace(/_/g, ' ') + }; + newChart.series.push(newSeries); + $('#seriesPreview').append('- ' + Functions.escapeHtml(newSeries.label + str) + '
    '); + newChart.nodes.push(serie); + $('#variableInput').val(''); + $('input[name="differentialValue"]').prop('checked', true); + $('input[name="useDivisor"]').prop('checked', false); + $('input[name="useUnit"]').prop('checked', false); + $('input[name="useDivisor"]').trigger('change'); + $('input[name="useUnit"]').trigger('change'); + $('select[name="varChartList"]').get(0).selectedIndex = 0; + + $('#clearSeriesLink').show(); + + return false; + }); + + $('#variableInput').autocomplete({ + source: variableNames + }); + + /* Initializes the monitor, called only once */ + function initGrid () { + var i; + + /* Apply default values & config */ + if (isStorageSupported('localStorage')) { + if (typeof window.localStorage.monitorCharts !== 'undefined') { + runtime.charts = JSON.parse(window.localStorage.monitorCharts); + } + if (typeof window.localStorage.monitorSettings !== 'undefined') { + monitorSettings = JSON.parse(window.localStorage.monitorSettings); + } + + $('a[href="#clearMonitorConfig"]').toggle(runtime.charts !== null); + + if (runtime.charts !== null + && typeof window.localStorage.monitorVersion !== 'undefined' + && monitorProtocolVersion !== window.localStorage.monitorVersion + ) { + $('#emptyDialog').dialog({ title: Messages.strIncompatibleMonitorConfig }); + $('#emptyDialog').html(Messages.strIncompatibleMonitorConfigDescription); + + var dlgBtns = {}; + dlgBtns[Messages.strClose] = function () { + $(this).dialog('close'); + }; + + $('#emptyDialog').dialog({ + width: 400, + buttons: dlgBtns + }); + } + } + + if (runtime.charts === null) { + runtime.charts = defaultChartGrid; + } + if (monitorSettings === null) { + monitorSettings = defaultMonitorSettings; + } + + $('select[name="gridChartRefresh"]').val(monitorSettings.gridRefresh / 1000); + $('select[name="chartColumns"]').val(monitorSettings.columns); + + if (monitorSettings.gridMaxPoints === 'auto') { + runtime.gridMaxPoints = Math.round((monitorSettings.chartSize.width - 40) / 12); + } else { + runtime.gridMaxPoints = monitorSettings.gridMaxPoints; + } + + runtime.xmin = new Date().getTime() - serverTimeDiff - runtime.gridMaxPoints * monitorSettings.gridRefresh; + runtime.xmax = new Date().getTime() - serverTimeDiff + monitorSettings.gridRefresh; + + /* Calculate how much spacing there is between each chart */ + $('#chartGrid').html(''); + chartSpacing = { + width: $('#chartGrid').find('td:nth-child(2)').offset().left - + $('#chartGrid').find('td:nth-child(1)').offset().left, + height: $('#chartGrid').find('tr:nth-child(2) td:nth-child(2)').offset().top - + $('#chartGrid').find('tr:nth-child(1) td:nth-child(1)').offset().top + }; + $('#chartGrid').html(''); + + /* Add all charts - in correct order */ + var keys = []; + $.each(runtime.charts, function (key) { + keys.push(key); + }); + keys.sort(); + for (i = 0; i < keys.length; i++) { + addChart(runtime.charts[keys[i]], true); + } + + /* Fill in missing cells */ + var numCharts = $('#chartGrid').find('.monitorChart').length; + var numMissingCells = (monitorSettings.columns - numCharts % monitorSettings.columns) % monitorSettings.columns; + for (i = 0; i < numMissingCells; i++) { + $('#chartGrid').find('tr:last').append(''); + } + + // Empty cells should keep their size so you can drop onto them + calculateChartSize(); + $('#chartGrid').find('tr td').css('width', chartSize.width + 'px'); + + buildRequiredDataList(); + refreshChartGrid(); + } + + /* Calls destroyGrid() and initGrid(), but before doing so it saves the chart + * data from each chart and restores it after the monitor is initialized again */ + function rebuildGrid () { + var oldData = null; + if (runtime.charts) { + oldData = {}; + $.each(runtime.charts, function (key, chartObj) { + for (var i = 0, l = chartObj.nodes.length; i < l; i++) { + oldData[chartObj.nodes[i].dataPoint] = []; + for (var j = 0, ll = chartObj.chart.series[i].data.length; j < ll; j++) { + oldData[chartObj.nodes[i].dataPoint].push([chartObj.chart.series[i].data[j].x, chartObj.chart.series[i].data[j].y]); + } + } + }); + } + + destroyGrid(); + initGrid(); + } + + /* Calculactes the dynamic chart size that depends on the column width */ + function calculateChartSize () { + var panelWidth; + if ($('body').height() > $(window).height()) { // has vertical scroll bar + panelWidth = $('#logTable').innerWidth(); + } else { + panelWidth = $('#logTable').innerWidth() - 10; // leave some space for vertical scroll bar + } + + var wdt = panelWidth; + var windowWidth = $(window).width(); + + if (windowWidth > 768) { + wdt = (panelWidth - monitorSettings.columns * chartSpacing.width) / monitorSettings.columns; + } + + chartSize = { + width: Math.floor(wdt), + height: Math.floor(0.75 * wdt) + }; + } + + /* Adds a chart to the chart grid */ + function addChart (chartObj, initialize) { + var i; + var settings = { + title: Functions.escapeHtml(chartObj.title), + grid: { + drawBorder: false, + shadow: false, + background: 'rgba(0,0,0,0)' + }, + axes: { + xaxis: { + renderer: $.jqplot.DateAxisRenderer, + tickOptions: { + formatString: '%H:%M:%S', + showGridline: false + }, + min: runtime.xmin, + max: runtime.xmax + }, + yaxis: { + min: 0, + max: 100, + tickInterval: 20 + } + }, + seriesDefaults: { + rendererOptions: { + smooth: true + }, + showLine: true, + lineWidth: 2, + markerOptions: { + size: 6 + } + }, + highlighter: { + show: true + } + }; + + if (settings.title === Messages.strSystemCPUUsage || + settings.title === Messages.strQueryCacheEfficiency + ) { + settings.axes.yaxis.tickOptions = { + formatString: '%d %%' + }; + } else if (settings.title === Messages.strSystemMemory || + settings.title === Messages.strSystemSwap + ) { + settings.stackSeries = true; + settings.axes.yaxis.tickOptions = { + formatter: $.jqplot.byteFormatter(2) // MiB + }; + } else if (settings.title === Messages.strTraffic) { + settings.axes.yaxis.tickOptions = { + formatter: $.jqplot.byteFormatter(1) // KiB + }; + } else if (settings.title === Messages.strQuestions || + settings.title === Messages.strConnections + ) { + settings.axes.yaxis.tickOptions = { + formatter: function (format, val) { + if (Math.abs(val) >= 1000000) { + return $.jqplot.sprintf('%.3g M', val / 1000000); + } else if (Math.abs(val) >= 1000) { + return $.jqplot.sprintf('%.3g k', val / 1000); + } else { + return $.jqplot.sprintf('%d', val); + } + } + }; + } + + settings.series = chartObj.series; + + if ($('#' + 'gridchart' + runtime.chartAI).length === 0) { + var numCharts = $('#chartGrid').find('.monitorChart').length; + + if (numCharts === 0 || (numCharts % monitorSettings.columns === 0)) { + $('#chartGrid').append(''); + } + + if (!chartSize) { + calculateChartSize(); + } + $('#chartGrid').find('tr:last').append( + '
    ' + + '
    ' + + '
    ' + ); + } + + // Set series' data as [0,0], smooth lines won't plot with data array having null values. + // also chart won't plot initially with no data and data comes on refreshChartGrid() + var series = []; + for (i in chartObj.series) { + series.push([[0, 0]]); + } + + var tempTooltipContentEditor = function (str, seriesIndex, pointIndex, plot) { + var j; + // TODO: move style to theme CSS + var tooltipHtml = '
    '; + // x value i.e. time + var timeValue = str.split(',')[0]; + var seriesValue; + tooltipHtml += 'Time: ' + timeValue; + tooltipHtml += ''; + // Add y values to the tooltip per series + for (j in plot.series) { + // get y value if present + if (plot.series[j].data.length > pointIndex) { + seriesValue = plot.series[j].data[pointIndex][1]; + } else { + return; + } + var seriesLabel = plot.series[j].label; + var seriesColor = plot.series[j].color; + // format y value + if (plot.series[0]._yaxis.tickOptions.formatter) { // eslint-disable-line no-underscore-dangle + // using formatter function + // eslint-disable-next-line no-underscore-dangle + seriesValue = plot.series[0]._yaxis.tickOptions.formatter('%s', seriesValue); + } else if (plot.series[0]._yaxis.tickOptions.formatString) { // eslint-disable-line no-underscore-dangle + // using format string + // eslint-disable-next-line no-underscore-dangle + seriesValue = Functions.sprintf(plot.series[0]._yaxis.tickOptions.formatString, seriesValue); + } + tooltipHtml += '
    ' + + seriesLabel + ': ' + seriesValue + ''; + } + tooltipHtml += '
    '; + return tooltipHtml; + }; + + // set Tooltip for each series + for (i in settings.series) { + settings.series[i].highlighter = { + show: true, + tooltipContentEditor: tempTooltipContentEditor + }; + } + + chartObj.chart = $.jqplot('gridchart' + runtime.chartAI, series, settings); + // remove [0,0] after plotting + for (i in chartObj.chart.series) { + chartObj.chart.series[i].data.shift(); + } + + var $legend = $('
    ').css('padding', '0.5em'); + for (i in chartObj.chart.series) { + $legend.append( + $('
    ').append( + $('
    ').css({ + width: '1em', + height: '1em', + background: chartObj.chart.seriesColors[i] + }).addClass('floatleft') + ).append( + $('
    ').text( + chartObj.chart.series[i].label + ).addClass('floatleft') + ).append( + $('
    ') + ).addClass('floatleft') + ); + } + $('#gridchart' + runtime.chartAI) + .parent() + .append($legend); + + if (initialize !== true) { + runtime.charts['c' + runtime.chartAI] = chartObj; + buildRequiredDataList(); + } + + // time span selection + $('#gridchart' + runtime.chartAI).on('jqplotMouseDown', function (ev, gridpos, datapos) { + drawTimeSpan = true; + selectionTimeDiff.push(datapos.xaxis); + if ($('#selection_box').length) { + $('#selection_box').remove(); + } + var selectionBox = $('
    '); + $(document.body).append(selectionBox); + selectionStartX = ev.pageX; + selectionStartY = ev.pageY; + selectionBox + .attr({ id: 'selection_box' }) + .css({ + top: selectionStartY - gridpos.y, + left: selectionStartX + }) + .fadeIn(); + }); + + $('#gridchart' + runtime.chartAI).on('jqplotMouseUp', function (ev, gridpos, datapos) { + if (! drawTimeSpan || editMode) { + return; + } + + selectionTimeDiff.push(datapos.xaxis); + + if (selectionTimeDiff[1] <= selectionTimeDiff[0]) { + selectionTimeDiff = []; + return; + } + // get date from timestamp + var min = new Date(Math.ceil(selectionTimeDiff[0])); + var max = new Date(Math.ceil(selectionTimeDiff[1])); + getLogAnalyseDialog(min, max); + selectionTimeDiff = []; + drawTimeSpan = false; + }); + + $('#gridchart' + runtime.chartAI).on('jqplotMouseMove', function (ev) { + if (! drawTimeSpan || editMode) { + return; + } + if (selectionStartX !== undefined) { + $('#selection_box') + .css({ + width: Math.ceil(ev.pageX - selectionStartX) + }) + .fadeIn(); + } + }); + + $('#gridchart' + runtime.chartAI).on('jqplotMouseLeave', function () { + drawTimeSpan = false; + }); + + $(document.body).on('mouseup', function () { + if ($('#selection_box').length) { + $('#selection_box').remove(); + } + }); + + // Edit, Print icon only in edit mode + $('#chartGrid').find('div svg').find('*[zIndex=20], *[zIndex=21], *[zIndex=19]').toggle(editMode); + + runtime.chartAI++; + } + + function getLogAnalyseDialog (min, max) { + var $logAnalyseDialog = $('#logAnalyseDialog'); + var $dateStart = $logAnalyseDialog.find('input[name="dateStart"]'); + var $dateEnd = $logAnalyseDialog.find('input[name="dateEnd"]'); + $dateStart.prop('readonly', true); + $dateEnd.prop('readonly', true); + + var dlgBtns = { }; + + dlgBtns[Messages.strFromSlowLog] = function () { + loadLog('slow', min, max); + $(this).dialog('close'); + }; + + dlgBtns[Messages.strFromGeneralLog] = function () { + loadLog('general', min, max); + $(this).dialog('close'); + }; + + $logAnalyseDialog.dialog({ + width: 'auto', + height: 'auto', + buttons: dlgBtns + }); + + Functions.addDatepicker($dateStart, 'datetime', { + showMillisec: false, + showMicrosec: false, + timeFormat: 'HH:mm:ss' + }); + Functions.addDatepicker($dateEnd, 'datetime', { + showMillisec: false, + showMicrosec: false, + timeFormat: 'HH:mm:ss' + }); + $dateStart.datepicker('setDate', min); + $dateEnd.datepicker('setDate', max); + } + + function loadLog (type, min, max) { + var dateStart = Date.parse($('#logAnalyseDialog').find('input[name="dateStart"]').datepicker('getDate')) || min; + var dateEnd = Date.parse($('#logAnalyseDialog').find('input[name="dateEnd"]').datepicker('getDate')) || max; + + loadLogStatistics({ + src: type, + start: dateStart, + end: dateEnd, + removeVariables: $('#removeVariables').prop('checked'), + limitTypes: $('#limitTypes').prop('checked') + }); + } + + /* Called in regular intervals, this function updates the values of each chart in the grid */ + function refreshChartGrid () { + /* Send to server */ + runtime.refreshRequest = $.post('server_status_monitor.php' + CommonParams.get('common_query'), { + 'ajax_request': true, + 'chart_data': 1, + 'type': 'chartgrid', + 'requiredData': JSON.stringify(runtime.dataList), + 'server': CommonParams.get('server') + }, function (data) { + var chartData; + if (typeof data !== 'undefined' && data.success === true) { + chartData = data.message; + } else { + return serverResponseError(); + } + var value; + var i = 0; + var diff; + var total; + + /* Update values in each graph */ + $.each(runtime.charts, function (orderKey, elem) { + var key = elem.chartID; + // If newly added chart, we have no data for it yet + if (! chartData[key]) { + return; + } + // Draw all series + total = 0; + for (var j = 0; j < elem.nodes.length; j++) { + // Update x-axis + if (i === 0 && j === 0) { + if (oldChartData === null) { + diff = chartData.x - runtime.xmax; + } else { + diff = parseInt(chartData.x - oldChartData.x, 10); + } + + runtime.xmin += diff; + runtime.xmax += diff; + } + + // elem.chart.xAxis[0].setExtremes(runtime.xmin, runtime.xmax, false); + /* Calculate y value */ + + // If transform function given, use it + if (elem.nodes[j].transformFn) { + value = chartValueTransform( + elem.nodes[j].transformFn, + chartData[key][j], + // Check if first iteration (oldChartData==null), or if newly added chart oldChartData[key]==null + ( + oldChartData === null || + oldChartData[key] === null || + oldChartData[key] === undefined ? null : oldChartData[key][j] + ) + ); + + // Otherwise use original value and apply differential and divisor if given, + // in this case we have only one data point per series - located at chartData[key][j][0] + } else { + value = parseFloat(chartData[key][j][0].value); + + if (elem.nodes[j].display === 'differential') { + if (oldChartData === null || + oldChartData[key] === null || + oldChartData[key] === undefined + ) { + continue; + } + value -= oldChartData[key][j][0].value; + } + + if (elem.nodes[j].valueDivisor) { + value = value / elem.nodes[j].valueDivisor; + } + } + + // Set y value, if defined + if (value !== undefined) { + elem.chart.series[j].data.push([chartData.x, value]); + if (value > elem.maxYLabel) { + elem.maxYLabel = value; + } else if (elem.maxYLabel === 0) { + elem.maxYLabel = 0.5; + } + // free old data point values and update maxYLabel + if (elem.chart.series[j].data.length > runtime.gridMaxPoints && + elem.chart.series[j].data[0][0] < runtime.xmin + ) { + // check if the next freeable point is highest + if (elem.maxYLabel <= elem.chart.series[j].data[0][1]) { + elem.chart.series[j].data.splice(0, elem.chart.series[j].data.length - runtime.gridMaxPoints); + elem.maxYLabel = getMaxYLabel(elem.chart.series[j].data); + } else { + elem.chart.series[j].data.splice(0, elem.chart.series[j].data.length - runtime.gridMaxPoints); + } + } + if (elem.title === Messages.strSystemMemory || + elem.title === Messages.strSystemSwap + ) { + total += value; + } + } + } + + // update chart options + // keep ticks number/positioning consistent while refreshrate changes + var tickInterval = (runtime.xmax - runtime.xmin) / 5; + elem.chart.axes.xaxis.ticks = [(runtime.xmax - tickInterval * 4), + (runtime.xmax - tickInterval * 3), (runtime.xmax - tickInterval * 2), + (runtime.xmax - tickInterval), runtime.xmax]; + + if (elem.title !== Messages.strSystemCPUUsage && + elem.title !== Messages.strQueryCacheEfficiency && + elem.title !== Messages.strSystemMemory && + elem.title !== Messages.strSystemSwap + ) { + elem.chart.axes.yaxis.max = Math.ceil(elem.maxYLabel * 1.1); + elem.chart.axes.yaxis.tickInterval = Math.ceil(elem.maxYLabel * 1.1 / 5); + } else if (elem.title === Messages.strSystemMemory || + elem.title === Messages.strSystemSwap + ) { + elem.chart.axes.yaxis.max = Math.ceil(total * 1.1 / 100) * 100; + elem.chart.axes.yaxis.tickInterval = Math.ceil(total * 1.1 / 5); + } + i++; + + if (runtime.redrawCharts) { + elem.chart.replot(); + } + }); + + oldChartData = chartData; + + runtime.refreshTimeout = setTimeout(refreshChartGrid, monitorSettings.gridRefresh); + }); + } + + /* Function to get highest plotted point's y label, to scale the chart, + * TODO: make jqplot's autoscale:true work here + */ + function getMaxYLabel (dataValues) { + var maxY = dataValues[0][1]; + $.each(dataValues, function (k, v) { + maxY = (v[1] > maxY) ? v[1] : maxY; + }); + return maxY; + } + + /* Function that supplies special value transform functions for chart values */ + function chartValueTransform (name, cur, prev) { + switch (name) { + case 'cpu-linux': + if (prev === null) { + return undefined; + } + // cur and prev are datapoint arrays, but containing + // only 1 element for cpu-linux + var newCur = cur[0]; + var newPrev = prev[0]; + + var diffTotal = newCur.busy + newCur.idle - (newPrev.busy + newPrev.idle); + var diffIdle = newCur.idle - newPrev.idle; + return 100 * (diffTotal - diffIdle) / diffTotal; + + // Query cache efficiency (%) + case 'qce': + if (prev === null) { + return undefined; + } + // cur[0].value is Qcache_hits, cur[1].value is Com_select + var diffQHits = cur[0].value - prev[0].value; + // No NaN please :-) + if (cur[1].value - prev[1].value === 0) { + return 0; + } + + return diffQHits / (cur[1].value - prev[1].value + diffQHits) * 100; + + // Query cache usage (%) + case 'qcu': + if (cur[1].value === 0) { + return 0; + } + // cur[0].value is Qcache_free_memory, cur[1].value is query_cache_size + return 100 - cur[0].value / cur[1].value * 100; + } + return undefined; + } + + /* Build list of nodes that need to be retrieved from server. + * It creates something like a stripped down version of the runtime.charts object. + */ + function buildRequiredDataList () { + runtime.dataList = {}; + // Store an own id, because the property name is subject of reordering, + // thus destroying our mapping with runtime.charts <=> runtime.dataList + var chartID = 0; + $.each(runtime.charts, function (key, chart) { + runtime.dataList[chartID] = []; + for (var i = 0, l = chart.nodes.length; i < l; i++) { + runtime.dataList[chartID][i] = chart.nodes[i].dataPoints; + } + runtime.charts[key].chartID = chartID; + chartID++; + }); + } + + /* Loads the log table data, generates the table and handles the filters */ + function loadLogStatistics (opts) { + var logRequest = null; + + if (! opts.removeVariables) { + opts.removeVariables = false; + } + if (! opts.limitTypes) { + opts.limitTypes = false; + } + + $('#emptyDialog').dialog({ title: Messages.strAnalysingLogsTitle }); + $('#emptyDialog').html(Messages.strAnalysingLogs + + ' '); + var dlgBtns = {}; + + dlgBtns[Messages.strCancelRequest] = function () { + if (logRequest !== null) { + logRequest.abort(); + } + + $(this).dialog('close'); + }; + + $('#emptyDialog').dialog({ + width: 'auto', + height: 'auto', + buttons: dlgBtns + }); + + logRequest = $.post( + 'server_status_monitor.php' + CommonParams.get('common_query'), + { + 'ajax_request': true, + 'log_data': 1, + 'type': opts.src, + 'time_start': Math.round(opts.start / 1000), + 'time_end': Math.round(opts.end / 1000), + 'removeVariables': opts.removeVariables, + 'limitTypes': opts.limitTypes + }, + function (data) { + var logData; + var dlgBtns = {}; + if (typeof data !== 'undefined' && data.success === true) { + logData = data.message; + } else { + return serverResponseError(); + } + + if (logData.rows.length === 0) { + $('#emptyDialog').dialog({ title: Messages.strNoDataFoundTitle }); + $('#emptyDialog').html('

    ' + Messages.strNoDataFound + '

    '); + + dlgBtns[Messages.strClose] = function () { + $(this).dialog('close'); + }; + + $('#emptyDialog').dialog('option', 'buttons', dlgBtns); + return; + } + + runtime.logDataCols = buildLogTable(logData, opts.removeVariables); + + /* Show some stats in the dialog */ + $('#emptyDialog').dialog({ title: Messages.strLoadingLogs }); + $('#emptyDialog').html('

    ' + Messages.strLogDataLoaded + '

    '); + $.each(logData.sum, function (key, value) { + var newKey = key.charAt(0).toUpperCase() + key.slice(1).toLowerCase(); + if (newKey === 'Total') { + newKey = '' + newKey + ''; + } + $('#emptyDialog').append(newKey + ': ' + value + '
    '); + }); + + /* Add filter options if more than a bunch of rows there to filter */ + if (logData.numRows > 12) { + $('#logTable').prepend( + '
    ' + + ' ' + Messages.strFiltersForLogTable + '' + + '
    ' + + ' ' + + ' ' + + '
    ' + + ((logData.numRows > 250) ? '
    ' : '') + + '
    ' + + ' ' + + ' ' + + ' ' + ); + + $('#noWHEREData').on('change', function () { + filterQueries(true); + }); + + if (logData.numRows > 250) { + $('#startFilterQueryText').on('click', filterQueries); + } else { + $('#filterQueryText').on('keyup', filterQueries); + } + } + + dlgBtns[Messages.strJumpToTable] = function () { + $(this).dialog('close'); + $(document).scrollTop($('#logTable').offset().top); + }; + + $('#emptyDialog').dialog('option', 'buttons', dlgBtns); + } + ); + + /* Handles the actions performed when the user uses any of the + * log table filters which are the filter by name and grouping + * with ignoring data in WHERE clauses + * + * @param boolean Should be true when the users enabled or disabled + * to group queries ignoring data in WHERE clauses + */ + function filterQueries (varFilterChange) { + var textFilter; + var val = $('#filterQueryText').val(); + + if (val.length === 0) { + textFilter = null; + } else { + try { + textFilter = new RegExp(val, 'i'); + $('#filterQueryText').removeClass('error'); + } catch (e) { + if (e instanceof SyntaxError) { + $('#filterQueryText').addClass('error'); + textFilter = null; + } + } + } + + var rowSum = 0; + var totalSum = 0; + var i = 0; + var q; + var noVars = $('#noWHEREData').prop('checked'); + var equalsFilter = /([^=]+)=(\d+|(('|"|).*?[^\\])\4((\s+)|$))/gi; + var functionFilter = /([a-z0-9_]+)\(.+?\)/gi; + var filteredQueries = {}; + var filteredQueriesLines = {}; + var hide = false; + var rowData; + var queryColumnName = runtime.logDataCols[runtime.logDataCols.length - 2]; + var sumColumnName = runtime.logDataCols[runtime.logDataCols.length - 1]; + var isSlowLog = opts.src === 'slow'; + var columnSums = {}; + + // For the slow log we have to count many columns (query_time, lock_time, rows_examined, rows_sent, etc.) + var countRow = function (query, row) { + var cells = row.match(/(.*?)<\/td>/gi); + if (!columnSums[query]) { + columnSums[query] = [0, 0, 0, 0]; + } + + // lock_time and query_time and displayed in timespan format + columnSums[query][0] += timeToSec(cells[2].replace(/(|<\/td>)/gi, '')); + columnSums[query][1] += timeToSec(cells[3].replace(/(|<\/td>)/gi, '')); + // rows_examind and rows_sent are just numbers + columnSums[query][2] += parseInt(cells[4].replace(/(|<\/td>)/gi, ''), 10); + columnSums[query][3] += parseInt(cells[5].replace(/(|<\/td>)/gi, ''), 10); + }; + + // We just assume the sql text is always in the second last column, and that the total count is right of it + $('#logTable').find('table tbody tr td:nth-child(' + (runtime.logDataCols.length - 1) + ')').each(function () { + var $t = $(this); + // If query is a SELECT and user enabled or disabled to group + // queries ignoring data in where statements, we + // need to re-calculate the sums of each row + if (varFilterChange && $t.html().match(/^SELECT/i)) { + if (noVars) { + // Group on => Sum up identical columns, and hide all but 1 + + q = $t.text().replace(equalsFilter, '$1=...$6').trim(); + q = q.replace(functionFilter, ' $1(...)'); + + // Js does not specify a limit on property name length, + // so we can abuse it as index :-) + if (filteredQueries[q]) { + filteredQueries[q] += parseInt($t.next().text(), 10); + totalSum += parseInt($t.next().text(), 10); + hide = true; + } else { + filteredQueries[q] = parseInt($t.next().text(), 10); + filteredQueriesLines[q] = i; + $t.text(q); + } + if (isSlowLog) { + countRow(q, $t.parent().html()); + } + } else { + // Group off: Restore original columns + + rowData = $t.parent().data('query'); + // Restore SQL text + $t.text(rowData[queryColumnName]); + // Restore total count + $t.next().text(rowData[sumColumnName]); + // Restore slow log columns + if (isSlowLog) { + $t.parent().children('td:nth-child(3)').text(rowData.query_time); + $t.parent().children('td:nth-child(4)').text(rowData.lock_time); + $t.parent().children('td:nth-child(5)').text(rowData.rows_sent); + $t.parent().children('td:nth-child(6)').text(rowData.rows_examined); + } + } + } + + // If not required to be hidden, do we need + // to hide because of a not matching text filter? + if (! hide && (textFilter !== null && ! textFilter.exec($t.text()))) { + hide = true; + } + + // Now display or hide this column + if (hide) { + $t.parent().css('display', 'none'); + } else { + totalSum += parseInt($t.next().text(), 10); + rowSum++; + $t.parent().css('display', ''); + } + + hide = false; + i++; + }); + + // We finished summarizing counts => Update count values of all grouped entries + if (varFilterChange) { + if (noVars) { + var numCol; + var row; + var $table = $('#logTable').find('table tbody'); + $.each(filteredQueriesLines, function (key, value) { + if (filteredQueries[key] <= 1) { + return; + } + + row = $table.children('tr:nth-child(' + (value + 1) + ')'); + numCol = row.children(':nth-child(' + (runtime.logDataCols.length) + ')'); + numCol.text(filteredQueries[key]); + + if (isSlowLog) { + row.children('td:nth-child(3)').text(secToTime(columnSums[key][0])); + row.children('td:nth-child(4)').text(secToTime(columnSums[key][1])); + row.children('td:nth-child(5)').text(columnSums[key][2]); + row.children('td:nth-child(6)').text(columnSums[key][3]); + } + }); + } + + $('#logTable').find('table').trigger('update'); + setTimeout(function () { + $('#logTable').find('table').trigger('sorton', [[[runtime.logDataCols.length - 1, 1]]]); + }, 0); + } + + // Display some stats at the bottom of the table + $('#logTable').find('table tfoot tr') + .html('' + + Messages.strSumRows + ' ' + rowSum + '' + + Messages.strTotal + '' + totalSum + ''); + } + } + + /* Turns a timespan (12:12:12) into a number */ + function timeToSec (timeStr) { + var time = timeStr.split(':'); + return (parseInt(time[0], 10) * 3600) + (parseInt(time[1], 10) * 60) + parseInt(time[2], 10); + } + + /* Turns a number into a timespan (100 into 00:01:40) */ + function secToTime (timeInt) { + var time = timeInt; + var hours = Math.floor(time / 3600); + time -= hours * 3600; + var minutes = Math.floor(time / 60); + time -= minutes * 60; + + if (hours < 10) { + hours = '0' + hours; + } + if (minutes < 10) { + minutes = '0' + minutes; + } + if (time < 10) { + time = '0' + time; + } + + return hours + ':' + minutes + ':' + time; + } + + /* Constructs the log table out of the retrieved server data */ + function buildLogTable (data, groupInserts) { + var rows = data.rows; + var cols = []; + var $table = $('
    '); + var $tBody; + var $tRow; + var $tCell; + + $('#logTable').html($table); + + var tempPushKey = function (key) { + cols.push(key); + }; + + var formatValue = function (name, value) { + if (name === 'user_host') { + return value.replace(/(\[.*?\])+/g, ''); + } + return Functions.escapeHtml(value); + }; + + for (var i = 0, l = rows.length; i < l; i++) { + if (i === 0) { + $.each(rows[0], tempPushKey); + $table.append('' + + '' + cols.join('') + '' + + '' + ); + + $table.append($tBody = $('')); + } + + $tBody.append($tRow = $('')); + for (var j = 0, ll = cols.length; j < ll; j++) { + // Assuming the query column is the second last + if (j === cols.length - 2 && rows[i][cols[j]].match(/^SELECT/i)) { + $tRow.append($tCell = $('' + formatValue(cols[j], rows[i][cols[j]]) + '')); + $tCell.on('click', openQueryAnalyzer); + } else { + $tRow.append('' + formatValue(cols[j], rows[i][cols[j]]) + ''); + } + + $tRow.data('query', rows[i]); + } + } + + $table.append('' + + '' + Messages.strSumRows + + ' ' + data.numRows + '' + Messages.strTotal + + '' + data.sum.TOTAL + ''); + + // Append a tooltip to the count column, if there exist one + if ($('#logTable').find('tr:first th:last').text().indexOf('#') > -1) { + $('#logTable').find('tr:first th:last').append(' ' + Functions.getImage('b_help', '', { 'class': 'qroupedQueryInfoIcon' })); + + var tooltipContent = Messages.strCountColumnExplanation; + if (groupInserts) { + tooltipContent += '

    ' + Messages.strMoreCountColumnExplanation + '

    '; + } + + Functions.tooltip( + $('img.qroupedQueryInfoIcon'), + 'img', + tooltipContent + ); + } + + $('#logTable').find('table').tablesorter({ + sortList: [[cols.length - 1, 1]], + widgets: ['fast-zebra'] + }); + + $('#logTable').find('table thead th') + .append('
    '); + + return cols; + } + + /* Opens the query analyzer dialog */ + function openQueryAnalyzer () { + var rowData = $(this).parent().data('query'); + var query = rowData.argument || rowData.sql_text; + + if (codeMirrorEditor) { + // TODO: somehow Functions.sqlPrettyPrint messes up the query, needs be fixed + // query = Functions.sqlPrettyPrint(query); + codeMirrorEditor.setValue(query); + // Codemirror is bugged, it doesn't refresh properly sometimes. + // Following lines seem to fix that + setTimeout(function () { + codeMirrorEditor.refresh(); + }, 50); + } else { + $('#sqlquery').val(query); + } + + var profilingChart = null; + var dlgBtns = {}; + + dlgBtns[Messages.strAnalyzeQuery] = function () { + profilingChart = loadQueryAnalysis(rowData); + }; + dlgBtns[Messages.strClose] = function () { + $(this).dialog('close'); + }; + + $('#queryAnalyzerDialog').dialog({ + width: 'auto', + height: 'auto', + resizable: false, + buttons: dlgBtns, + close: function () { + if (profilingChart !== null) { + profilingChart.destroy(); + } + $('#queryAnalyzerDialog').find('div.placeHolder').html(''); + if (codeMirrorEditor) { + codeMirrorEditor.setValue(''); + } else { + $('#sqlquery').val(''); + } + } + }); + } + + /* Loads and displays the analyzed query data */ + function loadQueryAnalysis (rowData) { + var db = rowData.db || ''; + var profilingChart = null; + + $('#queryAnalyzerDialog').find('div.placeHolder').html( + Messages.strAnalyzing + ' '); + + $.post('server_status_monitor.php' + CommonParams.get('common_query'), { + 'ajax_request': true, + 'query_analyzer': true, + 'query': codeMirrorEditor ? codeMirrorEditor.getValue() : $('#sqlquery').val(), + 'database': db, + 'server': CommonParams.get('server') + }, function (responseData) { + var data = responseData; + var i; + var l; + if (typeof data !== 'undefined' && data.success === true) { + data = data.message; + } + if (data.error) { + if (data.error.indexOf('1146') !== -1 || data.error.indexOf('1046') !== -1) { + data.error = Messages.strServerLogError; + } + $('#queryAnalyzerDialog').find('div.placeHolder').html('
    ' + data.error + '
    '); + return; + } + var totalTime = 0; + // Float sux, I'll use table :( + $('#queryAnalyzerDialog').find('div.placeHolder') + .html('
    '); + + var explain = '' + Messages.strExplainOutput + ' ' + $('#explain_docu').html(); + if (data.explain.length > 1) { + explain += ' ('; + for (i = 0; i < data.explain.length; i++) { + if (i > 0) { + explain += ', '; + } + explain += '' + i + ''; + } + explain += ')'; + } + explain += '

    '; + + var tempExplain = function (key, value) { + var newValue = (value === null) ? 'null' : Functions.escapeHtml(value); + + if (key === 'type' && newValue.toLowerCase() === 'all') { + newValue = '' + newValue + ''; + } + if (key === 'Extra') { + newValue = newValue.replace(/(using (temporary|filesort))/gi, '$1'); + } + explain += key + ': ' + newValue + '
    '; + }; + + for (i = 0, l = data.explain.length; i < l; i++) { + explain += '
    0 ? 'style="display:none;"' : '') + '>'; + $.each(data.explain[i], tempExplain); + explain += '
    '; + } + + explain += '

    ' + Messages.strAffectedRows + ' ' + data.affectedRows; + + $('#queryAnalyzerDialog').find('div.placeHolder td.explain').append(explain); + + $('#queryAnalyzerDialog').find('div.placeHolder a[href*="#showExplain"]').on('click', function () { + var id = $(this).attr('href').split('-')[1]; + $(this).parent().find('div[class*="explain"]').hide(); + $(this).parent().find('div[class*="explain-' + id + '"]').show(); + }); + + if (data.profiling) { + var chartData = []; + var numberTable = ''; + var duration; + var otherTime = 0; + + for (i = 0, l = data.profiling.length; i < l; i++) { + duration = parseFloat(data.profiling[i].duration); + + totalTime += duration; + + numberTable += ''; + } + + // Only put those values in the pie which are > 2% + for (i = 0, l = data.profiling.length; i < l; i++) { + duration = parseFloat(data.profiling[i].duration); + + if (duration / totalTime > 0.02) { + chartData.push([Functions.prettyProfilingNum(duration, 2) + ' ' + data.profiling[i].state, duration]); + } else { + otherTime += duration; + } + } + + if (otherTime > 0) { + chartData.push([Functions.prettyProfilingNum(otherTime, 2) + ' ' + Messages.strOther, otherTime]); + } + + numberTable += ''; + numberTable += '
    ' + Messages.strStatus + '' + Messages.strTime + '
    ' + data.profiling[i].state + ' ' + Functions.prettyProfilingNum(duration, 2) + '
    ' + Messages.strTotalTime + '' + Functions.prettyProfilingNum(totalTime, 2) + '
    '; + + $('#queryAnalyzerDialog').find('div.placeHolder td.chart').append( + '' + Messages.strProfilingResults + ' ' + $('#profiling_docu').html() + ' ' + + '(' + Messages.strTable + ', ' + Messages.strChart + ')
    ' + + numberTable + '

    '); + + $('#queryAnalyzerDialog').find('div.placeHolder a[href="#showNums"]').on('click', function () { + $('#queryAnalyzerDialog').find('#queryProfiling').hide(); + $('#queryAnalyzerDialog').find('table.queryNums').show(); + return false; + }); + + $('#queryAnalyzerDialog').find('div.placeHolder a[href="#showChart"]').on('click', function () { + $('#queryAnalyzerDialog').find('#queryProfiling').show(); + $('#queryAnalyzerDialog').find('table.queryNums').hide(); + return false; + }); + + profilingChart = Functions.createProfilingChart( + 'queryProfiling', + chartData + ); + } + }); + return profilingChart; + } + + /* Saves the monitor to localstorage */ + function saveMonitor () { + var gridCopy = {}; + + $.each(runtime.charts, function (key, elem) { + gridCopy[key] = {}; + gridCopy[key].nodes = elem.nodes; + gridCopy[key].settings = elem.settings; + gridCopy[key].title = elem.title; + gridCopy[key].series = elem.series; + gridCopy[key].maxYLabel = elem.maxYLabel; + }); + + if (isStorageSupported('localStorage')) { + window.localStorage.monitorCharts = JSON.stringify(gridCopy); + window.localStorage.monitorSettings = JSON.stringify(monitorSettings); + window.localStorage.monitorVersion = monitorProtocolVersion; + } + + $('a[href="#clearMonitorConfig"]').show(); + } +}); + +// Run the monitor once loaded +AJAX.registerOnload('server/status/monitor.js', function () { + $('a[href="#pauseCharts"]').trigger('click'); +}); + +function serverResponseError () { + var btns = {}; + btns[Messages.strReloadPage] = function () { + window.location.reload(); + }; + $('#emptyDialog').dialog({ title: Messages.strRefreshFailed }); + $('#emptyDialog').html( + Functions.getImage('s_attention') + + Messages.strInvalidResponseExplanation + ); + $('#emptyDialog').dialog({ buttons: btns }); +} + +/* Destroys all monitor related resources */ +function destroyGrid () { + if (runtime.charts) { + $.each(runtime.charts, function (key, value) { + try { + value.chart.destroy(); + } catch (err) { + // continue regardless of error + } + }); + } + + try { + runtime.refreshRequest.abort(); + } catch (err) { + // continue regardless of error + } + try { + clearTimeout(runtime.refreshTimeout); + } catch (err) { + // continue regardless of error + } + $('#chartGrid').html(''); + runtime.charts = null; + runtime.chartAI = 0; + monitorSettings = null; +} diff --git a/srcs/phpmyadmin/js/server/status/processes.js b/srcs/phpmyadmin/js/server/status/processes.js new file mode 100644 index 0000000..d4acebb --- /dev/null +++ b/srcs/phpmyadmin/js/server/status/processes.js @@ -0,0 +1,189 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Server Status Processes + * + * @package PhpMyAdmin + */ + +// object to store process list state information +var processList = { + + // denotes whether auto refresh is on or off + autoRefresh: false, + // stores the GET request which refresh process list + refreshRequest: null, + // stores the timeout id returned by setTimeout + refreshTimeout: null, + // the refresh interval in seconds + refreshInterval: null, + // the refresh URL (required to save last used option) + // i.e. full or sorting url + refreshUrl: null, + + /** + * Handles killing of a process + * + * @return void + */ + init: function () { + processList.setRefreshLabel(); + if (processList.refreshUrl === null) { + processList.refreshUrl = 'server_status_processes.php' + + CommonParams.get('common_query'); + } + if (processList.refreshInterval === null) { + processList.refreshInterval = $('#id_refreshRate').val(); + } else { + $('#id_refreshRate').val(processList.refreshInterval); + } + }, + + /** + * Handles killing of a process + * + * @param object the event object + * + * @return void + */ + killProcessHandler: function (event) { + event.preventDefault(); + var argSep = CommonParams.get('arg_separator'); + var params = $(this).getPostData(); + params += argSep + 'ajax_request=1' + argSep + 'server=' + CommonParams.get('server'); + // Get row element of the process to be killed. + var $tr = $(this).closest('tr'); + $.post($(this).attr('href'), params, function (data) { + // Check if process was killed or not. + if (data.hasOwnProperty('success') && data.success) { + // remove the row of killed process. + $tr.remove(); + // As we just removed a row, reapply odd-even classes + // to keep table stripes consistent + var $tableProcessListTr = $('#tableprocesslist').find('> tbody > tr'); + $tableProcessListTr.filter(':even').removeClass('odd').addClass('even'); + $tableProcessListTr.filter(':odd').removeClass('even').addClass('odd'); + // Show process killed message + Functions.ajaxShowMessage(data.message, false); + } else { + // Show process error message + Functions.ajaxShowMessage(data.error, false); + } + }, 'json'); + }, + + /** + * Handles Auto Refreshing + * + * @param object the event object + * + * @return void + */ + refresh: function () { + // abort any previous pending requests + // this is necessary, it may go into + // multiple loops causing unnecessary + // requests even after leaving the page. + processList.abortRefresh(); + // if auto refresh is enabled + if (processList.autoRefresh) { + var interval = parseInt(processList.refreshInterval, 10) * 1000; + var urlParams = processList.getUrlParams(); + processList.refreshRequest = $.post(processList.refreshUrl, + urlParams, + function (data) { + if (data.hasOwnProperty('success') && data.success) { + var $newTable = $(data.message); + $('#tableprocesslist').html($newTable.html()); + Functions.highlightSql($('#tableprocesslist')); + } + processList.refreshTimeout = setTimeout( + processList.refresh, + interval + ); + }); + } + }, + + /** + * Stop current request and clears timeout + * + * @return void + */ + abortRefresh: function () { + if (processList.refreshRequest !== null) { + processList.refreshRequest.abort(); + processList.refreshRequest = null; + } + clearTimeout(processList.refreshTimeout); + }, + + /** + * Set label of refresh button + * change between play & pause + * + * @return void + */ + setRefreshLabel: function () { + var img = 'play'; + var label = Messages.strStartRefresh; + if (processList.autoRefresh) { + img = 'pause'; + label = Messages.strStopRefresh; + processList.refresh(); + } + $('a#toggleRefresh').html(Functions.getImage(img) + Functions.escapeHtml(label)); + }, + + /** + * Return the Url Parameters + * for autorefresh request, + * includes showExecuting if the filter is checked + * + * @return urlParams - url parameters with autoRefresh request + */ + getUrlParams: function () { + var urlParams = { 'ajax_request': true, 'refresh': true }; + if ($('#showExecuting').is(':checked')) { + urlParams.showExecuting = true; + return urlParams; + } + return urlParams; + } +}; + +AJAX.registerOnload('server/status/processes.js', function () { + processList.init(); + // Bind event handler for kill_process + $('#tableprocesslist').on( + 'click', + 'a.kill_process', + processList.killProcessHandler + ); + // Bind event handler for toggling refresh of process list + $('a#toggleRefresh').on('click', function (event) { + event.preventDefault(); + processList.autoRefresh = !processList.autoRefresh; + processList.setRefreshLabel(); + }); + // Bind event handler for change in refresh rate + $('#id_refreshRate').on('change', function () { + processList.refreshInterval = $(this).val(); + processList.refresh(); + }); + // Bind event handler for table header links + $('#tableprocesslist').on('click', 'thead a', function () { + processList.refreshUrl = $(this).attr('href'); + }); +}); + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/status/processes.js', function () { + $('#tableprocesslist').off('click', 'a.kill_process'); + $('a#toggleRefresh').off('click'); + $('#id_refreshRate').off('change'); + $('#tableprocesslist').off('click', 'thead a'); + // stop refreshing further + processList.abortRefresh(); +}); diff --git a/srcs/phpmyadmin/js/server/status/queries.js b/srcs/phpmyadmin/js/server/status/queries.js new file mode 100644 index 0000000..960d307 --- /dev/null +++ b/srcs/phpmyadmin/js/server/status/queries.js @@ -0,0 +1,46 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview Javascript functions used in server status query page + * @name Server Status Query + * + * @requires jQuery + * @requires jQueryUI + * @requires js/functions.js + */ + +/* global initTableSorter */ // js/server/status/sorter.js + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/status/queries.js', function () { + if (document.getElementById('serverstatusquerieschart') !== null) { + var queryPieChart = $('#serverstatusquerieschart').data('queryPieChart'); + if (queryPieChart) { + queryPieChart.destroy(); + } + } +}); + +AJAX.registerOnload('server/status/queries.js', function () { + // Build query statistics chart + var cdata = []; + try { + if (document.getElementById('serverstatusquerieschart') !== null) { + $.each($('#serverstatusquerieschart').data('chart'), function (key, value) { + cdata.push([key, parseInt(value, 10)]); + }); + $('#serverstatusquerieschart').data( + 'queryPieChart', + Functions.createProfilingChart( + 'serverstatusquerieschart', + cdata + ) + ); + } + } catch (exception) { + // Could not load chart, no big deal... + } + + initTableSorter('statustabs_queries'); +}); diff --git a/srcs/phpmyadmin/js/server/status/sorter.js b/srcs/phpmyadmin/js/server/status/sorter.js new file mode 100644 index 0000000..49b6221 --- /dev/null +++ b/srcs/phpmyadmin/js/server/status/sorter.js @@ -0,0 +1,71 @@ +// TODO: tablesorter shouldn't sort already sorted columns +// eslint-disable-next-line no-unused-vars +function initTableSorter (tabid) { + var $table; + var opts; + switch (tabid) { + case 'statustabs_queries': + $table = $('#serverstatusqueriesdetails'); + opts = { + sortList: [[3, 1]], + headers: { + 1: { sorter: 'fancyNumber' }, + 2: { sorter: 'fancyNumber' } + } + }; + break; + } + $table.tablesorter(opts); + $table.find('tr:first th') + .append('
    '); +} + +$(function () { + $.tablesorter.addParser({ + id: 'fancyNumber', + is: function (s) { + return (/^[0-9]?[0-9,\\.]*\s?(k|M|G|T|%)?$/).test(s); + }, + format: function (s) { + var num = jQuery.tablesorter.formatFloat( + s.replace(Messages.strThousandsSeparator, '') + .replace(Messages.strDecimalSeparator, '.') + ); + + var factor = 1; + switch (s.charAt(s.length - 1)) { + case '%': + factor = -2; + break; + // Todo: Complete this list (as well as in the regexp a few lines up) + case 'k': + factor = 3; + break; + case 'M': + factor = 6; + break; + case 'G': + factor = 9; + break; + case 'T': + factor = 12; + break; + } + + return num * Math.pow(10, factor); + }, + type: 'numeric' + }); + + $.tablesorter.addParser({ + id: 'withinSpanNumber', + is: function (s) { + return (/(.*)?<\/span>/); + return (res && res.length >= 3) ? res[2] : 0; + }, + type: 'numeric' + }); +}); diff --git a/srcs/phpmyadmin/js/server/status/variables.js b/srcs/phpmyadmin/js/server/status/variables.js new file mode 100644 index 0000000..441aeeb --- /dev/null +++ b/srcs/phpmyadmin/js/server/status/variables.js @@ -0,0 +1,100 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * + * + * @package PhpMyAdmin + */ + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/status/variables.js', function () { + $('#filterAlert').off('change'); + $('#filterText').off('keyup'); + $('#filterCategory').off('change'); + $('#dontFormat').off('change'); +}); + +AJAX.registerOnload('server/status/variables.js', function () { + // Filters for status variables + var textFilter = null; + var alertFilter = $('#filterAlert').prop('checked'); + var categoryFilter = $('#filterCategory').find(':selected').val(); + var text = ''; // Holds filter text + + /* 3 Filtering functions */ + $('#filterAlert').on('change', function () { + alertFilter = this.checked; + filterVariables(); + }); + + $('#filterCategory').on('change', function () { + categoryFilter = $(this).val(); + filterVariables(); + }); + + $('#dontFormat').on('change', function () { + // Hiding the table while changing values speeds up the process a lot + $('#serverstatusvariables').hide(); + $('#serverstatusvariables').find('td.value span.original').toggle(this.checked); + $('#serverstatusvariables').find('td.value span.formatted').toggle(! this.checked); + $('#serverstatusvariables').show(); + }).trigger('change'); + + $('#filterText').on('keyup', function () { + var word = $(this).val().replace(/_/g, ' '); + if (word.length === 0) { + textFilter = null; + } else { + try { + textFilter = new RegExp('(^| )' + word, 'i'); + $(this).removeClass('error'); + } catch (e) { + if (e instanceof SyntaxError) { + $(this).addClass('error'); + textFilter = null; + } + } + } + text = word; + filterVariables(); + }).trigger('keyup'); + + /* Filters the status variables by name/category/alert in the variables tab */ + function filterVariables () { + var usefulLinks = 0; + var section = text; + + if (categoryFilter.length > 0) { + section = categoryFilter; + } + + if (section.length > 1) { + $('#linkSuggestions').find('span').each(function () { + if ($(this).attr('class').indexOf('status_' + section) !== -1) { + usefulLinks++; + $(this).css('display', ''); + } else { + $(this).css('display', 'none'); + } + }); + } + + if (usefulLinks > 0) { + $('#linkSuggestions').css('display', ''); + } else { + $('#linkSuggestions').css('display', 'none'); + } + + $('#serverstatusvariables').find('th.name').each(function () { + if ((textFilter === null || textFilter.exec($(this).text())) && + (! alertFilter || $(this).next().find('span.attention').length > 0) && + (categoryFilter.length === 0 || $(this).parent().hasClass('s_' + categoryFilter)) + ) { + $(this).parent().css('display', ''); + } else { + $(this).parent().css('display', 'none'); + } + }); + } +}); diff --git a/srcs/phpmyadmin/js/server/user_groups.js b/srcs/phpmyadmin/js/server/user_groups.js new file mode 100644 index 0000000..717a54a --- /dev/null +++ b/srcs/phpmyadmin/js/server/user_groups.js @@ -0,0 +1,52 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview Javascript functions used in server user groups page + * @name Server User Groups + * + * @requires jQuery + * @requires jQueryUI + */ + +/* global checkboxesSel */ // js/functions.js + +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/user_groups.js', function () { + $(document).off('click', 'a.deleteUserGroup.ajax'); +}); + +/** + * Bind event handlers + */ +AJAX.registerOnload('server/user_groups.js', function () { + // update the checkall checkbox on Edit user group page + $(checkboxesSel).trigger('change'); + + $(document).on('click', 'a.deleteUserGroup.ajax', function (event) { + event.preventDefault(); + var $link = $(this); + var groupName = $link.parents('tr').find('td:first').text(); + var buttonOptions = {}; + buttonOptions[Messages.strGo] = function () { + $(this).dialog('close'); + $link.removeClass('ajax').trigger('click'); + }; + buttonOptions[Messages.strClose] = function () { + $(this).dialog('close'); + }; + $('
    ') + .attr('id', 'confirmUserGroupDeleteDialog') + .append(Functions.sprintf(Messages.strDropUserGroupWarning, Functions.escapeHtml(groupName))) + .dialog({ + width: 300, + minWidth: 200, + modal: true, + buttons: buttonOptions, + title: Messages.strConfirm, + close: function () { + $(this).remove(); + } + }); + }); +}); diff --git a/srcs/phpmyadmin/js/server/variables.js b/srcs/phpmyadmin/js/server/variables.js new file mode 100644 index 0000000..523ef06 --- /dev/null +++ b/srcs/phpmyadmin/js/server/variables.js @@ -0,0 +1,118 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview Javascript functions used in server variables page + * @name Server Replication + * + * @requires jQuery + * @requires jQueryUI + * @requires js/functions.js + */ +/** + * Unbind all event handlers before tearing down a page + */ +AJAX.registerTeardown('server/variables.js', function () { + $(document).off('click', 'a.editLink'); + $('#serverVariables').find('.var-name').find('a img').remove(); +}); + +AJAX.registerOnload('server/variables.js', function () { + var $saveLink = $('a.saveLink'); + var $cancelLink = $('a.cancelLink'); + + $('#serverVariables').find('.var-name').find('a').append( + $('#docImage').clone().css('display', 'inline-block') + ); + + /* Launches the variable editor */ + $(document).on('click', 'a.editLink', function (event) { + event.preventDefault(); + editVariable(this); + }); + + /* Allows the user to edit a server variable */ + function editVariable (link) { + var $link = $(link); + var $cell = $link.parent(); + var $valueCell = $link.parents('.var-row').find('.var-value'); + var varName = $link.data('variable'); + + var $mySaveLink = $saveLink.clone().css('display', 'inline-block'); + var $myCancelLink = $cancelLink.clone().css('display', 'inline-block'); + var $msgbox = Functions.ajaxShowMessage(); + var $myEditLink = $cell.find('a.editLink'); + $cell.addClass('edit'); // variable is being edited + $myEditLink.remove(); // remove edit link + + $mySaveLink.on('click', function () { + var $msgbox = Functions.ajaxShowMessage(Messages.strProcessingRequest); + $.post($(this).attr('href'), { + 'ajax_request': true, + 'type': 'setval', + 'varName': varName, + 'varValue': $valueCell.find('input').val() + }, function (data) { + if (data.success) { + $valueCell + .html(data.variable) + .data('content', data.variable); + Functions.ajaxRemoveMessage($msgbox); + } else { + if (data.error === '') { + Functions.ajaxShowMessage(Messages.strRequestFailed, false); + } else { + Functions.ajaxShowMessage(data.error, false); + } + $valueCell.html($valueCell.data('content')); + } + $cell.removeClass('edit').html($myEditLink); + }); + return false; + }); + + $myCancelLink.on('click', function () { + $valueCell.html($valueCell.data('content')); + $cell.removeClass('edit').html($myEditLink); + return false; + }); + + $.get($mySaveLink.attr('href'), { + 'ajax_request': true, + 'type': 'getval', + 'varName': varName + }, function (data) { + if (typeof data !== 'undefined' && data.success === true) { + var $links = $('
    ') + .append($myCancelLink) + .append('   ') + .append($mySaveLink); + var $editor = $('
    ', { 'class': 'serverVariableEditor' }) + .append( + $('
    ').append( + $('', { type: 'text' }).val(data.message) + ) + ); + // Save and replace content + $cell + .html($links) + .children() + .css('display', 'flex'); + $valueCell + .data('content', $valueCell.html()) + .html($editor) + .find('input') + .trigger('focus') + .on('keydown', function (event) { // Keyboard shortcuts + if (event.keyCode === 13) { // Enter key + $mySaveLink.trigger('click'); + } else if (event.keyCode === 27) { // Escape key + $myCancelLink.trigger('click'); + } + }); + Functions.ajaxRemoveMessage($msgbox); + } else { + $cell.removeClass('edit').html($myEditLink); + Functions.ajaxShowMessage(data.error); + } + }); + } +}); -- cgit