From 62d16a07eef39f45bc74a86fc5692f6d5c5dc553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Meneghini=20Fauth?= Date: Thu, 6 Apr 2023 18:52:43 -0300 Subject: [PATCH] Replace jqPlot with Chart.js in /server/status/queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maurício Meneghini Fauth --- js/src/chart.ts | 12 +- js/src/server/status/queries.ts | 64 +++++------ js/src/server/status/sorter.ts | 78 ------------- .../JavaScriptMessagesController.php | 2 + .../Server/Status/MonitorController.php | 1 - .../Server/Status/QueriesController.php | 28 +++-- .../Server/Status/VariablesController.php | 1 - package.json | 2 + phpstan-baseline.neon | 5 + psalm-baseline.xml | 3 +- public/themes/bootstrap/scss/_common.scss | 17 --- public/themes/metro/scss/_common.scss | 12 -- public/themes/original/scss/_common.scss | 19 ---- public/themes/pmahomme/scss/_common.scss | 17 --- templates/server/status/queries/index.twig | 105 +++++++++--------- .../Server/Status/QueriesControllerTest.php | 65 +++-------- webpack.config.cjs | 2 +- yarn.lock | 24 ++++ 18 files changed, 147 insertions(+), 310 deletions(-) delete mode 100644 js/src/server/status/sorter.ts diff --git a/js/src/chart.ts b/js/src/chart.ts index dbd122e172..8ce6ade3b8 100644 --- a/js/src/chart.ts +++ b/js/src/chart.ts @@ -42,11 +42,11 @@ ChartFactory.prototype = { * @param elementId * id of the div element the chart is drawn in */ -var Chart = function (elementId = undefined) { +var AbstractChart = function (elementId = undefined) { this.elementId = elementId; }; -Chart.prototype = { +AbstractChart.prototype = { draw: function () { throw new Error('draw must be implemented by a subclass'); }, @@ -74,10 +74,10 @@ Chart.prototype = { * id of the div element the chart is drawn in */ var BaseChart = function (elementId = undefined) { - Chart.call(this, elementId); + AbstractChart.call(this, elementId); }; -BaseChart.prototype = new Chart(); +BaseChart.prototype = new AbstractChart(); BaseChart.prototype.constructor = BaseChart; BaseChart.prototype.validateColumns = function (dataTable) { var columns = dataTable.getColumns(); @@ -220,12 +220,12 @@ var DataTable = function () { * id of the div element the chart is drawn in */ var JQPlotChart = function (elementId = undefined) { - Chart.call(this, elementId); + AbstractChart.call(this, elementId); this.plot = null; this.validator = null; }; -JQPlotChart.prototype = new Chart(); +JQPlotChart.prototype = new AbstractChart(); JQPlotChart.prototype.constructor = JQPlotChart; JQPlotChart.prototype.draw = function (data, options) { if (this.validator.validateColumns(data)) { diff --git a/js/src/server/status/queries.ts b/js/src/server/status/queries.ts index 75ef5b3533..c83de348b2 100644 --- a/js/src/server/status/queries.ts +++ b/js/src/server/status/queries.ts @@ -1,43 +1,37 @@ -import $ from 'jquery'; import { AJAX } from '../../modules/ajax.ts'; -import createProfilingChart from '../../modules/functions/createProfilingChart.ts'; -/** - * @fileoverview Javascript functions used in server status query page - * @name Server Status Query - * - * @requires jQueryUI - */ - -/** - * 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(); - } +function buildQueryStatsChart (): void { + const queryStatisticsChartCanvas = document.getElementById('query-statistics-chart') as HTMLCanvasElement; + if (! queryStatisticsChartCanvas) { + return; } -}); -AJAX.registerOnload('server/status/queries.js', function () { - // Build query statistics chart - var cdata = []; + const chartDataJson = queryStatisticsChartCanvas.getAttribute('data-chart-data'); + let chartData = null; try { - if (document.getElementById('serverstatusquerieschart') !== null) { - $.each($('#serverstatusquerieschart').data('chart'), function (key, value) { - cdata.push([key, parseInt(value, 10)]); - }); - - $('#serverstatusquerieschart').data( - 'queryPieChart', - createProfilingChart('serverstatusquerieschart', cdata) - ); - } - } catch (exception) { - // Could not load chart, no big deal... + chartData = JSON.parse(chartDataJson); + } catch (e) { + return; } - window.initTableSorter('statustabs_queries'); + if (! (chartData && 'labels' in chartData && 'data' in chartData)) { + return; + } + + new window.Chart(queryStatisticsChartCanvas, { + type: 'pie', + data: { + labels: chartData.labels, + datasets: [ + { + label: window.Messages.numberOfStatements, + data: chartData.data, + } + ], + }, + }); +} + +AJAX.registerOnload('server/status/queries.js', function (): void { + buildQueryStatsChart(); }); diff --git a/js/src/server/status/sorter.ts b/js/src/server/status/sorter.ts deleted file mode 100644 index 65f8f8aef1..0000000000 --- a/js/src/server/status/sorter.ts +++ /dev/null @@ -1,78 +0,0 @@ -import $ from 'jquery'; - -// 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().find('th') - .append('
'); -} - -window.initTableSorter = initTableSorter; - -$(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 = $.tablesorter.formatFloat( - s.replace(window.Messages.strThousandsSeparator, '') - .replace(window.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/libraries/classes/Controllers/JavaScriptMessagesController.php b/libraries/classes/Controllers/JavaScriptMessagesController.php index 02678caff4..a3980bb8e7 100644 --- a/libraries/classes/Controllers/JavaScriptMessagesController.php +++ b/libraries/classes/Controllers/JavaScriptMessagesController.php @@ -170,6 +170,8 @@ final class JavaScriptMessagesController 'strChartConnectionsTitle' => __('Connections / Processes'), + 'numberOfStatements' => __('Number of statements'), + /* server status monitor */ 'strIncompatibleMonitorConfig' => __('Local monitor configuration incompatible!'), 'strIncompatibleMonitorConfigDescription' => __( diff --git a/libraries/classes/Controllers/Server/Status/MonitorController.php b/libraries/classes/Controllers/Server/Status/MonitorController.php index 46cda34a0d..4a051db273 100644 --- a/libraries/classes/Controllers/Server/Status/MonitorController.php +++ b/libraries/classes/Controllers/Server/Status/MonitorController.php @@ -47,7 +47,6 @@ class MonitorController extends AbstractController 'vendor/jqplot/plugins/jqplot.cursor.js', 'jqplot/plugins/jqplot.byteFormatter.js', 'server/status/monitor.js', - 'server/status/sorter.js', 'chart.js',// Needed by createProfilingChart in server/status/monitor.js ]); diff --git a/libraries/classes/Controllers/Server/Status/QueriesController.php b/libraries/classes/Controllers/Server/Status/QueriesController.php index 9bdad0e04d..eb05aab3c5 100644 --- a/libraries/classes/Controllers/Server/Status/QueriesController.php +++ b/libraries/classes/Controllers/Server/Status/QueriesController.php @@ -15,11 +15,16 @@ use PhpMyAdmin\Template; use PhpMyAdmin\Url; use function __; +use function array_keys; use function array_sum; +use function array_values; use function arsort; use function count; +use function mb_convert_case; use function str_replace; +use const MB_CASE_TITLE; + class QueriesController extends AbstractController { public function __construct( @@ -39,17 +44,9 @@ class QueriesController extends AbstractController $this->dbi->selectDb('mysql'); } - $this->addScriptFiles([ - 'chart.js', - 'vendor/jqplot/jquery.jqplot.js', - 'vendor/jqplot/plugins/jqplot.pieRenderer.js', - 'vendor/jqplot/plugins/jqplot.highlighter.js', - 'vendor/jqplot/plugins/jqplot.enhancedPieLegendRenderer.js', - 'vendor/jquery/jquery.tablesorter.js', - 'server/status/sorter.js', - 'server/status/queries.js', - ]); + $this->addScriptFiles(['vendor/chart.umd.js', 'server/status/queries.js']); + $chart = []; if ($this->data->dataLoaded) { $hourFactor = 3600 / $this->data->status['Uptime']; $usedQueries = $this->data->usedQueries; @@ -65,7 +62,6 @@ class QueriesController extends AbstractController // reverse sort by value to show most used statements first arsort($usedQueries); - $chart = []; $querySum = array_sum($usedQueries); $otherSum = 0; $queries = []; @@ -73,13 +69,13 @@ class QueriesController extends AbstractController // For the percentage column, use Questions - Connections, because // the number of connections is not an item of the Query types // but is included in Questions. Then the total of the percentages is 100. - $name = str_replace(['Com_', '_'], ['', ' '], $key); + $name = mb_convert_case(str_replace(['Com_', '_'], ['', ' '], $key), MB_CASE_TITLE); // Group together values that make out less than 2% into "Other", but only // if we have more than 6 fractions already if ($value < $querySum * 0.02 && count($chart) > 6) { $otherSum += $value; } else { - $chart[$name] = $value; + $chart[$name] = (int) $value; } $queries[$key] = [ @@ -91,15 +87,17 @@ class QueriesController extends AbstractController } if ($otherSum > 0) { - $chart[__('Other')] = $otherSum; + $chart[__('Other statements')] = $otherSum; } } + $chartData = ['labels' => array_keys($chart), 'data' => array_values($chart)]; + $this->render('server/status/queries/index', [ 'is_data_loaded' => $this->data->dataLoaded, 'stats' => $stats ?? null, 'queries' => $queries ?? [], - 'chart' => $chart ?? [], + 'chart_data' => $chartData, ]); } } diff --git a/libraries/classes/Controllers/Server/Status/VariablesController.php b/libraries/classes/Controllers/Server/Status/VariablesController.php index 7aa5c71796..28b177f36d 100644 --- a/libraries/classes/Controllers/Server/Status/VariablesController.php +++ b/libraries/classes/Controllers/Server/Status/VariablesController.php @@ -48,7 +48,6 @@ class VariablesController extends AbstractController $this->addScriptFiles([ 'server/status/variables.js', 'vendor/jquery/jquery.tablesorter.js', - 'server/status/sorter.js', ]); $flush = $request->getParsedBodyParam('flush'); diff --git a/package.json b/package.json index 643818431e..c3b487d4ab 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@babel/preset-typescript": "^7.21.0", "@popperjs/core": "^2.11.7", "@types/bootstrap": "^5.2.6", + "@types/chart.js": "^2.9.37", "@types/codemirror": "^5.60.7", "@types/jquery": "^3.5.16", "@types/jquery.ui.datetimepicker": "^0.3.30", @@ -27,6 +28,7 @@ "autoprefixer": "^10.4.14", "babel-loader": "^9.1.2", "bootstrap": "5.3.0-alpha2", + "chart.js": "^4.2.1", "codemirror": "5.65.12", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.6.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dec478a262..498762f1cf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1950,6 +1950,11 @@ parameters: count: 2 path: libraries/classes/Controllers/Server/Status/ProcessesController.php + - + message: "#^Cannot cast mixed to int\\.$#" + count: 1 + path: libraries/classes/Controllers/Server/Status/QueriesController.php + - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, \\(int\\|string\\) given\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c859b9bc4f..61cc31a2da 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -2987,8 +2987,7 @@ $key - $chart[$name] - + $hourFactor $otherSum $value diff --git a/public/themes/bootstrap/scss/_common.scss b/public/themes/bootstrap/scss/_common.scss index 72da4209bd..fbb043ae97 100644 --- a/public/themes/bootstrap/scss/_common.scss +++ b/public/themes/bootstrap/scss/_common.scss @@ -607,11 +607,6 @@ div#tablestatistics table { cursor: pointer; } -h3#serverstatusqueries span { - font-size: 60%; - display: inline; -} - div#serverStatusTabs { margin-top: 1em; } @@ -620,13 +615,6 @@ caption a.top { float: right; } -div#serverstatusquerieschart { - float: left; - width: 500px; - height: 350px; - margin-right: 50px; -} - div { &#serverstatus table { tbody td.descr a, @@ -2666,11 +2654,6 @@ body .ui-dialog { width: 98%; } - div#serverstatusquerieschart { - width: 100%; - height: 450px; - } - .ui-dialog { margin: 1%; width: 95% !important; diff --git a/public/themes/metro/scss/_common.scss b/public/themes/metro/scss/_common.scss index d28da49998..4188fb2c03 100644 --- a/public/themes/metro/scss/_common.scss +++ b/public/themes/metro/scss/_common.scss @@ -817,11 +817,6 @@ div#tablestatistics table { cursor: pointer; } -h3#serverstatusqueries span { - font-size: 60%; - display: inline; -} - div#serverStatusTabs { margin-top: 1em; } @@ -830,13 +825,6 @@ caption a.top { float: right; } -div#serverstatusquerieschart { - float: left; - width: 500px; - height: 350px; - padding-left: 30px; -} - div { &#serverstatus table { tbody td.descr a, diff --git a/public/themes/original/scss/_common.scss b/public/themes/original/scss/_common.scss index 46d6f8cbee..29bcce0ee6 100644 --- a/public/themes/original/scss/_common.scss +++ b/public/themes/original/scss/_common.scss @@ -638,11 +638,6 @@ div#tablestatistics table { cursor: pointer; } -h3#serverstatusqueries span { - font-size: 60%; - display: inline; -} - div#serverStatusTabs { margin-top: 1em; } @@ -651,15 +646,6 @@ caption a.top { float: right; } -div { - &#serverstatusquerieschart { - float: left; - width: 500px; - height: 350px; - margin-right: 50px; - } -} - div { &#serverstatus table { tbody td.descr a, @@ -2672,11 +2658,6 @@ body { width: 98%; } - div#serverstatusquerieschart { - width: 100%; - height: 450px; - } - .ui-dialog { margin: 1%; width: 95% !important; diff --git a/public/themes/pmahomme/scss/_common.scss b/public/themes/pmahomme/scss/_common.scss index 7f77495eac..b186bb8739 100644 --- a/public/themes/pmahomme/scss/_common.scss +++ b/public/themes/pmahomme/scss/_common.scss @@ -780,11 +780,6 @@ div#tablestatistics table { cursor: pointer; } -h3#serverstatusqueries span { - font-size: 60%; - display: inline; -} - div#serverStatusTabs { margin-top: 1em; } @@ -793,13 +788,6 @@ caption a.top { float: right; } -div#serverstatusquerieschart { - float: left; - width: 500px; - height: 350px; - margin-right: 50px; -} - div { &#serverstatus table { tbody td.descr a, @@ -2802,11 +2790,6 @@ body .ui-dialog { width: 98%; } - div#serverstatusquerieschart { - width: 100%; - height: 450px; - } - .ui-dialog { margin: 1%; width: 95% !important; diff --git a/templates/server/status/queries/index.twig b/templates/server/status/queries/index.twig index 1d46b19a4d..aacaa3cd61 100644 --- a/templates/server/status/queries/index.twig +++ b/templates/server/status/queries/index.twig @@ -1,62 +1,57 @@ {% extends 'server/status/base.twig' %} {% set active = 'queries' %} {% block content %} +
+ {% if not is_data_loaded %} + {{ 'Not enough privilege to view query statistics.'|trans|error }} + {% else %} +

+ {% trans %}Questions since startup:{% notes %}Questions is the name of a MySQL Status variable{% endtrans %} {{ format_number(stats.total, 0) }} + {{ show_mysql_docu('server-status-variables', false, null, null, 'statvar_Questions') }} +

-{% if is_data_loaded %} -
-

- {% trans %} - Questions since startup: - {% notes %} - Questions is the name of a MySQL Status variable - {% endtrans %} - {{ format_number(stats.total, 0) }} - {{ show_mysql_docu('server-status-variables', false, null, null, 'statvar_Questions') }} -

-
+
+
    +
  • ø {% trans 'per hour:' %} {{ format_number(stats.per_hour, 0) }}
  • +
  • ø {% trans 'per minute:' %} {{ format_number(stats.per_minute, 0) }}
  • + {% if stats.per_second >= 1 %} +
  • ø {% trans 'per second:' %} {{ format_number(stats.per_second, 0) }}
  • + {% endif %} +
+
-
-
    -
  • ø {% trans 'per hour:' %} {{ format_number(stats.per_hour, 0) }}
  • -
  • ø {% trans 'per minute:' %} {{ format_number(stats.per_minute, 0) }}
  • - {% if stats.per_second >= 1 %} -
  • ø {% trans 'per second:' %} {{ format_number(stats.per_second, 0) }}
  • +
    + + + + + + + + + + + + + + + + + {% for query in queries %} + + + + + + + {% endfor %} + +
    {% trans 'Statements' %}{% trans %}#{% notes %}# = Amount of queries{% endtrans %}{% trans 'ø per hour' %}%
    {{ query.name }}{{ format_number(query.value, 5, 0, true) }}{{ format_number(query.per_hour, 4, 1, true) }}{{ format_number(query.percentage, 0, 2) }}
    + +
    + +
    +
    {% endif %} -
-
- -
- - - - - - - - - - - - - - - - - {% for query in queries %} - - - - - - - {% endfor %} - -
{% trans 'Statements' %}{% trans %}#{% notes %}# = Amount of queries{% endtrans %}{% trans 'ø per hour' %}%
{{ query.name }}{{ format_number(query.value, 5, 0, true) }}{{ format_number(query.per_hour, 4, 1, true) }}{{ format_number(query.percentage, 0, 2) }}
- -
-
-{% else %} - {{ 'Not enough privilege to view query statistics.'|trans|error }} -{% endif %} - +
{% endblock %} diff --git a/test/classes/Controllers/Server/Status/QueriesControllerTest.php b/test/classes/Controllers/Server/Status/QueriesControllerTest.php index 8c76c46825..d998d066a2 100644 --- a/test/classes/Controllers/Server/Status/QueriesControllerTest.php +++ b/test/classes/Controllers/Server/Status/QueriesControllerTest.php @@ -74,65 +74,28 @@ class QueriesControllerTest extends AbstractTestCase $usedQueries = $this->data->usedQueries; $totalQueries = array_sum($usedQueries); - $questionsFromStart = __('Questions since startup:') - . ' ' . Util::formatNumber($totalQueries, 0); + $questionsFromStart = __('Questions since startup:') . ' ' . Util::formatNumber($totalQueries, 0); - $this->assertStringContainsString('

', $html); + $this->assertStringContainsString('

', $html); $this->assertStringContainsString($questionsFromStart, $html); - $this->assertStringContainsString( - __('per hour:'), - $html, - ); - $this->assertStringContainsString( - Util::formatNumber($totalQueries * $hourFactor, 0), - $html, - ); + $this->assertStringContainsString('per hour:', $html); + $this->assertStringContainsString(Util::formatNumber($totalQueries * $hourFactor, 0), $html); $valuePerMinute = Util::formatNumber($totalQueries * 60 / $this->data->status['Uptime'], 0); - $this->assertStringContainsString( - __('per minute:'), - $html, - ); - $this->assertStringContainsString( - htmlspecialchars($valuePerMinute), - $html, - ); + $this->assertStringContainsString('per minute:', $html); + $this->assertStringContainsString(htmlspecialchars($valuePerMinute), $html); - $this->assertStringContainsString( - __('Statements'), - $html, - ); + $this->assertStringContainsString('Statements', $html); - $this->assertStringContainsString( - htmlspecialchars('change db'), - $html, - ); + $this->assertStringContainsString('Change Db', $html); $this->assertStringContainsString('54', $html); - $this->assertStringContainsString( - htmlspecialchars('select'), - $html, - ); - $this->assertStringContainsString( - htmlspecialchars('set option'), - $html, - ); - $this->assertStringContainsString( - htmlspecialchars('show databases'), - $html, - ); - $this->assertStringContainsString( - htmlspecialchars('show status'), - $html, - ); - $this->assertStringContainsString( - htmlspecialchars('show tables'), - $html, - ); + $this->assertStringContainsString('Select', $html); + $this->assertStringContainsString('Set Option', $html); + $this->assertStringContainsString('Show Databases', $html); + $this->assertStringContainsString('Show Status', $html); + $this->assertStringContainsString('Show Tables', $html); - $this->assertStringContainsString( - '
=3.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.4.0: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -4175,6 +4194,11 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +moment@^2.10.2: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"