diff --git a/js/src/ajax.js b/js/src/ajax.js index 0897884595..2837f733b8 100644 --- a/js/src/ajax.js +++ b/js/src/ajax.js @@ -586,7 +586,8 @@ export let AJAX = { * @todo This condition is to be removed once all the files are modularised */ if (checkNewCode(file)) { - var fileImports = ['server_privileges', 'server_databases']; + var fileImports = ['server_privileges', 'server_databases', 'server_status_advisor', + 'server_status_processes', 'server_status_variables',]; if ($.inArray(file, fileImports) !== -1) { // Dynamic import to load the files dynamically // This is used for the purpose of code splitting diff --git a/js/src/classes/ErrorReport.js b/js/src/classes/ErrorReport.js index 0aac56ab2d..87fd2caa2a 100644 --- a/js/src/classes/ErrorReport.js +++ b/js/src/classes/ErrorReport.js @@ -268,13 +268,11 @@ var ErrorReport = { * @return function */ wrap_function: function (func) { - // console.log(func.wrapped); if (!func.wrapped) { var new_func = function () { try { return func.apply(this, arguments); } catch (x) { - // console.log(new Error(x)); TraceKit.report(x); } }; @@ -284,7 +282,6 @@ var ErrorReport = { new_func.guid = func.guid = func.guid || new_func.guid || jQuery.guid++; return new_func; } else { - // console.log(func); return func; } }, diff --git a/js/src/classes/Server/ProcessList.js b/js/src/classes/Server/ProcessList.js new file mode 100644 index 0000000000..dc730aac86 --- /dev/null +++ b/js/src/classes/Server/ProcessList.js @@ -0,0 +1,164 @@ +import { PMA_ajaxShowMessage } from '../../utils/show_ajax_messages'; +import { PMA_highlightSQL } from '../../utils/sql'; +import { PMA_Messages as PMA_messages } from '../../variables/export_variables'; +import { PMA_commonParams } from '../../variables/common_params'; +import { PMA_getImage } from '../../functions/get_image'; +import { escapeHtml } from '../../utils/Sanitise'; +import { jQuery as $ } from '../../utils/JqueryExtended'; +// object to store process list state information +class ProcessList { + constructor () { + // denotes whether auto refresh is on or off + this.autoRefresh = false; + // stores the GET request which refresh process list + this.refreshRequest = null; + // stores the timeout id returned by setTimeout + this.refreshTimeout = null; + // the refresh interval in seconds + this.refreshInterval = null; + // the refresh URL (required to save last used option) + // i.e. full or sorting url + this.refreshUrl = null; + this.init = this.init.bind(this); + this.killProcessHandler = this.killProcessHandler.bind(this); + this.refresh = this.refresh.bind(this); + this.abortRefresh = this.abortRefresh.bind(this); + this.setRefreshLabel = this.setRefreshLabel.bind(this); + this.getUrlParams = this.getUrlParams.bind(this); + } + /** + * Handles killing of a process + * + * @return void + */ + init () { + this.setRefreshLabel(); + if (this.refreshUrl === null) { + this.refreshUrl = 'server_status_processes.php' + + PMA_commonParams.get('common_query'); + } + if (this.refreshInterval === null) { + this.refreshInterval = $('#id_refreshRate').val(); + } else { + $('#id_refreshRate').val(this.refreshInterval); + } + } + + /** + * Handles killing of a process + * + * @param object the event object + * + * @return void + */ + killProcessHandler (event, elementRef) { + event.preventDefault(); + var url = $(elementRef).attr('href'); + // Get row element of the process to be killed. + var $tr = $(elementRef).closest('tr'); + $.getJSON(url, 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 + PMA_ajaxShowMessage(data.message, false); + } else { + // Show process error message + PMA_ajaxShowMessage(data.error, false); + } + }); + } + + /** + * Handles Auto Refreshing + * + * @param object the event object + * + * @return void + */ + refresh () { + // abort any previous pending requests + // this is necessary, it may go into + // multiple loops causing unnecessary + // requests even after leaving the page. + this.abortRefresh(); + // if auto refresh is enabled + if (this.autoRefresh) { + var interval = parseInt(this.refreshInterval, 10) * 1000; + var urlParams = this.getUrlParams(); + this.refreshRequest = $.get(this.refreshUrl, + urlParams, + function (data) { + if (data.hasOwnProperty('success') && data.success) { + var $newTable = $(data.message); + $('#tableprocesslist').html($newTable.html()); + PMA_highlightSQL($('#tableprocesslist')); + } + this.refreshTimeout = setTimeout( + this.refresh, + interval + ); + }.bind(this)); + } + } + + /** + * Stop current request and clears timeout + * + * @return void + */ + abortRefresh () { + if (this.refreshRequest !== null) { + this.refreshRequest.abort(); + this.refreshRequest = null; + } + clearTimeout(this.refreshTimeout); + } + + /** + * Set label of refresh button + * change between play & pause + * + * @return void + */ + setRefreshLabel () { + var img = 'play'; + var label = PMA_messages.strStartRefresh; + if (this.autoRefresh) { + img = 'pause'; + label = PMA_messages.strStopRefresh; + this.refresh(); + } + $('a#toggleRefresh').html(PMA_getImage(img) + escapeHtml(label)); + } + + /** + * Return the Url Parameters + * for autorefresh request, + * includes showExecuting if the filter is checked + * + * @return urlParams - url parameters with autoRefresh request + */ + getUrlParams () { + var urlParams = { 'ajax_request': true, 'refresh': true }; + if ($('#showExecuting').is(':checked')) { + urlParams.showExecuting = true; + return urlParams; + } + return urlParams; + } +} + +/** + * Instance is exported to ensure that every time the import + * will have the instance only and not the class itself + */ +let processList = new ProcessList(); + +export default processList; diff --git a/js/src/consts/files.js b/js/src/consts/files.js index 930c3b3735..3ed5752da1 100644 --- a/js/src/consts/files.js +++ b/js/src/consts/files.js @@ -8,7 +8,10 @@ */ const files = { server_privileges: ['server_privileges'], - server_databases: ['server_databases'] + server_databases: ['server_databases'], + server_status_advisor: ['server_status_advisor'], + server_status_processes: ['server_status_processes'], + server_status_variables: ['server_status_variables'], }; export default files; diff --git a/js/src/server_privileges.js b/js/src/server_privileges.js index 8bbc69c386..eca080c5c0 100644 --- a/js/src/server_privileges.js +++ b/js/src/server_privileges.js @@ -15,6 +15,7 @@ import { PMA_Messages as PMA_messages } from './variables/export_variables'; import { PMA_ajaxShowMessage, PMA_ajaxRemoveMessage } from './utils/show_ajax_messages'; import { PMA_commonParams } from './variables/common_params'; import { jQuery as $ } from './utils/JqueryExtended'; +import { PMA_getSQLEditor } from './utils/sql'; /** * AJAX scripts for server_privileges page. diff --git a/js/src/server_status_advisor.js b/js/src/server_status_advisor.js new file mode 100644 index 0000000000..f3eb162d21 --- /dev/null +++ b/js/src/server_status_advisor.js @@ -0,0 +1,102 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Server Status Advisor + * + * @package PhpMyAdmin + */ + +import { PMA_Messages as PMA_messages } from './variables/export_variables'; + +/** + * Unbind all event handlers before tearing down a page + */ +export function teardown1 () { + $('a[href="#openAdvisorInstructions"]').off('click'); + $('#statustabs_advisor').html(''); + $('#advisorDialog').remove(); + $('#instructionsDialog').remove(); +} + +export function onload1 () { + // 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"]').click(function () { + var dlgBtns = {}; + dlgBtns[PMA_messages.strClose] = function () { + $(this).dialog('close'); + }; + $instructionsDialog.dialog({ + title: PMA_messages.strAdvisorSystem, + width: 700, + 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('

' + PMA_messages.strPerformanceIssues + '

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

' + PMA_messages.strIssuse + ':
' + rule.issue + '

' + + '

' + PMA_messages.strRecommendation + ':
' + rule.recommendation + '

' + + '

' + PMA_messages.strJustification + ':
' + rule.justification + '

' + + '

' + PMA_messages.strFormula + ':
' + rule.formula + '

' + + '

' + PMA_messages.strTest + ':
' + rule.test + '

' + ); + + var dlgBtns = {}; + dlgBtns[PMA_messages.strClose] = function () { + $(this).dialog('close'); + }; + + $dialog.dialog({ width: 600, buttons: dlgBtns }); + }); + }); + } +} diff --git a/js/src/server_status_processes.js b/js/src/server_status_processes.js new file mode 100644 index 0000000000..ee6be9b162 --- /dev/null +++ b/js/src/server_status_processes.js @@ -0,0 +1,41 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Server Status Processes + * + * @package PhpMyAdmin + */ +import processList from './classes/Server/ProcessList'; +export function onload1 () { + processList.init(); + // Bind event handler for kill_process + $('#tableprocesslist').on('click', 'a.kill_process', function (event) { + processList.killProcessHandler(event, this); + }); + // 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 (event) { + 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 + */ +export function teardown1 () { + $('#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/js/src/server_status_variables.js b/js/src/server_status_variables.js new file mode 100644 index 0000000000..b2db6e80b4 --- /dev/null +++ b/js/src/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 + */ +export function teardown1 () { + $('#filterAlert').off('change'); + $('#filterText').off('keyup'); + $('#filterCategory').off('change'); + $('#dontFormat').off('change'); +} + +export function onload1 () { + // 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').change(function () { + alertFilter = this.checked; + filterVariables(); + }); + + $('#filterCategory').change(function () { + categoryFilter = $(this).val(); + filterVariables(); + }); + + $('#dontFormat').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').keyup(function (e) { + 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 useful_links = 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) { + useful_links++; + $(this).css('display', ''); + } else { + $(this).css('display', 'none'); + } + }); + } + + if (useful_links > 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/js/src/utils/sql.js b/js/src/utils/sql.js new file mode 100644 index 0000000000..b842503792 --- /dev/null +++ b/js/src/utils/sql.js @@ -0,0 +1,272 @@ +import CodeMirror from 'codemirror'; +import '../../../node_modules/codemirror/mode/sql/sql.js'; +import '../../../node_modules/codemirror/addon/runmode/runmode.js'; +import '../../../node_modules/codemirror/addon/hint/show-hint.js'; +import '../../../node_modules/codemirror/addon/hint/sql-hint.js'; +import '../../../node_modules/codemirror/addon/lint/lint.js'; +// import '../../../node_modules/codemirror/addon/lint/sql-lint.js'; +import { mysql_doc_builtin, mysql_doc_keyword } from '../consts/doclinks'; + +window.cm = CodeMirror; + +/** + * Adds doc link to single highlighted SQL element + */ +function PMA_doc_add ($elm, params) { + if (typeof mysql_doc_template === 'undefined') { + return; + } + + var url = PMA_sprintf( + decodeURIComponent(mysql_doc_template), + params[0] + ); + if (params.length > 1) { + url += '#' + params[1]; + } + var content = $elm.text(); + $elm.text(''); + $elm.append('' + content + ''); +} + +/** + * Generates doc links for keywords inside highlighted SQL + */ +function PMA_doc_keyword (idx, elm) { + var $elm = $(elm); + /* Skip already processed ones */ + if ($elm.find('a').length > 0) { + return; + } + var keyword = $elm.text().toUpperCase(); + var $next = $elm.next('.cm-keyword'); + if ($next) { + var next_keyword = $next.text().toUpperCase(); + var full = keyword + ' ' + next_keyword; + + var $next2 = $next.next('.cm-keyword'); + if ($next2) { + var next2_keyword = $next2.text().toUpperCase(); + var full2 = full + ' ' + next2_keyword; + if (full2 in mysql_doc_keyword) { + PMA_doc_add($elm, mysql_doc_keyword[full2]); + PMA_doc_add($next, mysql_doc_keyword[full2]); + PMA_doc_add($next2, mysql_doc_keyword[full2]); + return; + } + } + if (full in mysql_doc_keyword) { + PMA_doc_add($elm, mysql_doc_keyword[full]); + PMA_doc_add($next, mysql_doc_keyword[full]); + return; + } + } + if (keyword in mysql_doc_keyword) { + PMA_doc_add($elm, mysql_doc_keyword[keyword]); + } +} + +/** + * Generates doc links for builtins inside highlighted SQL + */ +function PMA_doc_builtin (idx, elm) { + var $elm = $(elm); + var builtin = $elm.text().toUpperCase(); + if (builtin in mysql_doc_builtin) { + PMA_doc_add($elm, mysql_doc_builtin[builtin]); + } +} + +/** + * Higlights SQL using CodeMirror. + */ +export function PMA_highlightSQL ($base) { + var $elm = $base.find('code.sql'); + $elm.each(function () { + var $sql = $(this); + var $pre = $sql.find('pre'); + /* We only care about visible elements to avoid double processing */ + if ($pre.is(':visible')) { + var $highlight = $('
'); + $sql.append($highlight); + if (typeof CodeMirror !== 'undefined') { + CodeMirror.runMode($sql.text(), 'text/x-mysql', $highlight[0]); + $pre.hide(); + $highlight.find('.cm-keyword').each(PMA_doc_keyword); + $highlight.find('.cm-builtin').each(PMA_doc_builtin); + } + } + }); +} + + +/** + * "inputRead" event handler for CodeMirror SQL query editors for autocompletion + */ +let sql_autocomplete_in_progress = false; +let sql_autocomplete = false; +var sql_autocomplete_default_table = ''; +export function codemirrorAutocompleteOnInputRead (instance) { + if (!sql_autocomplete_in_progress + && (!instance.options.hintOptions.tables || !sql_autocomplete)) { + if (!sql_autocomplete) { + // Reset after teardown + instance.options.hintOptions.tables = false; + instance.options.hintOptions.defaultTable = ''; + + sql_autocomplete_in_progress = true; + + var href = 'db_sql_autocomplete.php'; + var params = { + 'ajax_request': true, + 'server': PMA_commonParams.get('server'), + 'db': PMA_commonParams.get('db'), + 'no_debug': true + }; + + var columnHintRender = function (elem, self, data) { + $('
') + .text(data.columnName) + .appendTo(elem); + $('
') + .text(data.columnHint) + .appendTo(elem); + }; + + $.ajax({ + type: 'POST', + url: href, + data: params, + success: function (data) { + if (data.success) { + var tables = JSON.parse(data.tables); + sql_autocomplete_default_table = PMA_commonParams.get('table'); + sql_autocomplete = []; + for (var table in tables) { + if (tables.hasOwnProperty(table)) { + var columns = tables[table]; + table = { + text: table, + columns: [] + }; + for (var column in columns) { + if (columns.hasOwnProperty(column)) { + var displayText = columns[column].Type; + if (columns[column].Key === 'PRI') { + displayText += ' | Primary'; + } else if (columns[column].Key === 'UNI') { + displayText += ' | Unique'; + } + table.columns.push({ + text: column, + displayText: column + ' | ' + displayText, + columnName: column, + columnHint: displayText, + render: columnHintRender + }); + } + } + } + sql_autocomplete.push(table); + } + instance.options.hintOptions.tables = sql_autocomplete; + instance.options.hintOptions.defaultTable = sql_autocomplete_default_table; + } + }, + complete: function () { + sql_autocomplete_in_progress = false; + } + }); + } else { + instance.options.hintOptions.tables = sql_autocomplete; + instance.options.hintOptions.defaultTable = sql_autocomplete_default_table; + } + } + if (instance.state.completionActive) { + return; + } + var cur = instance.getCursor(); + var token = instance.getTokenAt(cur); + var string = ''; + if (token.string.match(/^[.`\w@]\w*$/)) { + string = token.string; + } + if (string.length > 0) { + CodeMirror.commands.autocomplete(instance); + } +} + +/** + * Creates an SQL editor which supports auto completing etc. + * + * @param $textarea jQuery object wrapping the textarea to be made the editor + * @param options optional options for CodeMirror + * @param resize optional resizing ('vertical', 'horizontal', 'both') + * @param lintOptions additional options for lint + */ +export function PMA_getSQLEditor ($textarea, options, resize, lintOptions) { + if ($textarea.length > 0 && typeof CodeMirror !== 'undefined') { + // merge options for CodeMirror + var defaults = { + lineNumbers: true, + matchBrackets: true, + extraKeys: { 'Ctrl-Space': 'autocomplete' }, + hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true }, + indentUnit: 4, + mode: 'text/x-mysql', + lineWrapping: true + }; + + if (CodeMirror.sqlLint) { + $.extend(defaults, { + gutters: ['CodeMirror-lint-markers'], + lint: { + 'getAnnotations': CodeMirror.sqlLint, + 'async': true, + 'lintOptions': lintOptions + } + }); + } + + $.extend(true, defaults, options); + + // create CodeMirror editor + var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults); + // allow resizing + if (! resize) { + resize = 'vertical'; + } + var handles = ''; + if (resize === 'vertical') { + handles = 's'; + } + if (resize === 'both') { + handles = 'all'; + } + if (resize === 'horizontal') { + handles = 'e, w'; + } + $(codemirrorEditor.getWrapperElement()) + .css('resize', resize) + .resizable({ + handles: handles, + resize: function () { + codemirrorEditor.setSize($(this).width(), $(this).height()); + } + }); + // enable autocomplete + codemirrorEditor.on('inputRead', codemirrorAutocompleteOnInputRead); + + // page locking + codemirrorEditor.on('change', function (e) { + e.data = { + value: 3, + content: codemirrorEditor.isClean(), + }; + AJAX.lockPageHandler(e); + }); + + return codemirrorEditor; + } + return null; +} diff --git a/js/src/variables/common_params.js b/js/src/variables/common_params.js index 1446992f16..bfbfd21ac2 100644 --- a/js/src/variables/common_params.js +++ b/js/src/variables/common_params.js @@ -30,7 +30,6 @@ export const PMA_commonParams = (function () { if (i === 'db' || i === 'table') { updateNavigation = true; } - // reload = true; } params[i] = obj[i]; } diff --git a/server_status_advisor.php b/server_status_advisor.php index 7827a90b6d..0d717dc4d7 100644 --- a/server_status_advisor.php +++ b/server_status_advisor.php @@ -19,7 +19,7 @@ $serverStatusData = new Data(); $response = Response::getInstance(); $scripts = $response->getHeader()->getScripts(); -$scripts->addFile('server_status_advisor.js'); +$scripts->addFile('server_status_advisor'); /** * Output diff --git a/server_status_processes.php b/server_status_processes.php index e46b07a8ad..c8d5bf2e31 100644 --- a/server_status_processes.php +++ b/server_status_processes.php @@ -52,7 +52,7 @@ if ($response->isAjax() && !empty($_REQUEST['kill'])) { // Load the full page $header = $response->getHeader(); $scripts = $header->getScripts(); - $scripts->addFile('server_status_processes.js'); + $scripts->addFile('server_status_processes'); $response->addHTML('
'); $response->addHTML($serverStatusData->getMenuHtml()); $response->addHTML(Processes::getHtmlForProcessListFilter()); diff --git a/server_status_variables.php b/server_status_variables.php index e8dc61e6f7..328cbb43f6 100644 --- a/server_status_variables.php +++ b/server_status_variables.php @@ -37,7 +37,7 @@ $serverStatusData = new Data(); $response = Response::getInstance(); $header = $response->getHeader(); $scripts = $header->getScripts(); -$scripts->addFile('server_status_variables.js'); +$scripts->addFile('server_status_variables'); $scripts->addFile('vendor/jquery/jquery.tablesorter.js'); $scripts->addFile('server_status_sorter.js');