From 344c236d726eab5daa0a0e5d91f575927f932403 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Tue, 17 Oct 2023 15:01:52 +0530 Subject: [PATCH] Fix following issues in system stats: 1. Graphs rendering in opposite directions on tab change. 2. Y-axis label width should be dynamic. 3. Tooltip values should be formatted. --- web/pgadmin/dashboard/__init__.py | 3 + .../dashboard/static/js/SystemStats/CPU.jsx | 8 +- .../static/js/SystemStats/Memory.jsx | 6 +- .../static/js/SystemStats/Storage.jsx | 53 +++--- .../static/js/SystemStats/utility.js | 6 +- .../js/components/PgChart/StreamingChart.jsx | 161 ++++++++---------- web/pgadmin/static/js/utils.js | 4 +- 7 files changed, 112 insertions(+), 129 deletions(-) diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py index b88f313e805..ae102d783ab 100644 --- a/web/pgadmin/dashboard/__init__.py +++ b/web/pgadmin/dashboard/__init__.py @@ -659,6 +659,9 @@ def system_statistics(sid=None, did=None): ) status, res = g.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=str(res)) + for chart_row in res['rows']: resp_data[chart_row['chart_name']] = json.loads( chart_row['chart_data']) diff --git a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx index 29ac6aff8dd..9b1c75c5a7e 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx @@ -169,10 +169,10 @@ export default function CPU({preferences, sid, did, pageVisible, enablePoll=true setErrorMsg(null); if(data.hasOwnProperty('cpu_stats')){ let new_cu_stats = { - 'User Normal': data['cpu_stats']['usermode_normal_process_percent']?data['cpu_stats']['usermode_normal_process_percent']:0, - 'User Niced': data['cpu_stats']['usermode_niced_process_percent']?data['cpu_stats']['usermode_niced_process_percent']:0, - 'Kernel': data['cpu_stats']['kernelmode_process_percent']?data['cpu_stats']['kernelmode_process_percent']:0, - 'Idle': data['cpu_stats']['idle_mode_percent']?data['cpu_stats']['idle_mode_percent']:0, + 'User Normal': data['cpu_stats']['usermode_normal_process_percent'] ?? 0, + 'User Niced': data['cpu_stats']['usermode_niced_process_percent'] ?? 0, + 'Kernel': data['cpu_stats']['kernelmode_process_percent'] ?? 0, + 'Idle': data['cpu_stats']['idle_mode_percent'] ?? 0, }; cpuUsageInfoReduce({incoming: new_cu_stats}); } diff --git a/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx index d3b9d7ae3ab..f7f59963120 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx @@ -270,12 +270,14 @@ export function MemoryWrapper(props) { - + - + diff --git a/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx b/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx index b77920ac056..af4260adbf4 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx +++ b/web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx @@ -409,6 +409,26 @@ export function StorageWrapper(props) { lineBorderWidth: props.lineBorderWidth, }), [props.showTooltip, props.showDataPoints, props.lineBorderWidth]); + const chartJsExtraOptions = { + plugins: { + legend: { + display: false, + }, + tooltip: { + animation: false, + callbacks: { + title: function (context) { + const label = context[0].label || ''; + return label; + }, + label: function (context) { + return (context.dataset?.label ?? 'Total space: ') + toPrettySize(context.raw); + }, + }, + }, + }, + }; + return ( <> @@ -440,23 +460,7 @@ export function StorageWrapper(props) { }} options={{ animation: false, - plugins: { - legend: { - display: false, - }, - tooltip: { - callbacks: { - title: function (context) { - const label = context[0].label || ''; - return label; - }, - label: function (context) { - const value = context.formattedValue || 0; - return 'Total space: ' + value; - }, - }, - }, - }, + ...chartJsExtraOptions, }} /> @@ -502,11 +506,7 @@ export function StorageWrapper(props) { }, }, }, - plugins: { - legend: { - display: false, - }, - }, + ...chartJsExtraOptions, } } /> @@ -524,8 +524,11 @@ export function StorageWrapper(props) { {Object.keys(props.ioInfo[drive]).map((type, innerKeyIndex) => ( - - + + { + return type.endsWith('_time_rw') ? toPrettySize(v, 'ms') : toPrettySize(v); + }} /> ))} @@ -556,4 +559,4 @@ StorageWrapper.propTypes = { lineBorderWidth: PropTypes.number.isRequired, isDatabase: PropTypes.bool.isRequired, isTest: PropTypes.bool, -}; \ No newline at end of file +}; diff --git a/web/pgadmin/dashboard/static/js/SystemStats/utility.js b/web/pgadmin/dashboard/static/js/SystemStats/utility.js index 408b91b3696..730aad62609 100644 --- a/web/pgadmin/dashboard/static/js/SystemStats/utility.js +++ b/web/pgadmin/dashboard/static/js/SystemStats/utility.js @@ -60,14 +60,16 @@ export function statsReducer(state, action) { let newState = {}; Object.keys(action.incoming).forEach(label => { + // Sys stats extension may send 'NaN' sometimes, better handle it. + const value = action.incoming[label] == 'NaN' ? 0 : action.incoming[label]; if(state[label]) { newState[label] = [ - action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label], + action.counter ? value - action.counterData[label] :value, ...state[label].slice(0, X_AXIS_LENGTH-1), ]; } else { newState[label] = [ - action.counter ? action.incoming[label] - action.counterData[label] : action.incoming[label], + action.counter ? value - action.counterData[label] : value, ]; } }); diff --git a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx index 12a32d06785..50052b9f02c 100644 --- a/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx +++ b/web/pgadmin/static/js/components/PgChart/StreamingChart.jsx @@ -5,51 +5,22 @@ import gettext from 'sources/gettext'; import PropTypes from 'prop-types'; import { useTheme } from '@material-ui/styles'; -const removeExistingTooltips = () => { - // Select all elements with the class name "uplot-tooltip" - const tooltipLabels = document.querySelectorAll('.uplot-tooltip'); - - // Remove each selected element - tooltipLabels.forEach((tooltipLabel) => { - tooltipLabel.remove(); - }); -}; - -function formatLabel(ticks) { - // Format the label - return ticks.map((value) => { - if(value < 1){ - return value+''; - } - return parseLabel(value); - }); -} - -function parseLabel(label) { - const suffixes = ['', 'k', 'M', 'B', 'T']; - const suffixNum = Math.floor(Math.log10(label) / 3); - const shortValue = (label / Math.pow(1000, suffixNum)).toFixed(1); - return shortValue + ' ' + suffixes[suffixNum]; -} - function tooltipPlugin(refreshRate) { let tooltipTopOffset = -20; let tooltipLeftOffset = 10; - let tooltip; function showTooltip() { - if(!tooltip) { - removeExistingTooltips(); - tooltip = document.createElement('div'); - tooltip.className = 'uplot-tooltip'; - tooltip.style.display = 'block'; - document.body.appendChild(tooltip); + if(!window.uplotTooltip) { + window.uplotTooltip = document.createElement('div'); + window.uplotTooltip.className = 'uplot-tooltip'; + window.uplotTooltip.style.display = 'block'; + document.body.appendChild(window.uplotTooltip); } } function hideTooltip() { - tooltip?.remove(); - tooltip = null; + window.uplotTooltip?.remove(); + window.uplotTooltip = null; } function setTooltip(u) { @@ -61,21 +32,19 @@ function tooltipPlugin(refreshRate) { let tooltipHtml=`
${(u.data[1].length-1-parseInt(u.legend.values[0]['_'])) * refreshRate + gettext(' seconds ago')}
`; for(let i=1; i 1) _tooltip = parseLabel(_tooltip); - tooltipHtml += `
${u.series[i].label}: ${_tooltip}
`; + tooltipHtml += `
${u.series[i].label}: ${u.legend.values[i]['_']}
`; } - tooltip.innerHTML = tooltipHtml; + window.uplotTooltip.innerHTML = tooltipHtml; let overBBox = u.over.getBoundingClientRect(); - let tooltipBBox = tooltip.getBoundingClientRect(); + let tooltipBBox = window.uplotTooltip.getBoundingClientRect(); let left = (tooltipLeftOffset + u.cursor.left + overBBox.left); /* Should not outside the graph right */ if((left+tooltipBBox.width) > overBBox.right) { left = left - tooltipBBox.width - tooltipLeftOffset*2; } - tooltip.style.left = left + 'px'; - tooltip.style.top = (tooltipTopOffset + u.cursor.top + overBBox.top) + 'px'; + window.uplotTooltip.style.left = left + 'px'; + window.uplotTooltip.style.top = (tooltipTopOffset + u.cursor.top + overBBox.top) + 'px'; } return { @@ -89,7 +58,7 @@ function tooltipPlugin(refreshRate) { }; } -export default function StreamingChart({xRange=75, data, options, showSecondAxis=false}) { +export default function StreamingChart({xRange=75, data, options, valueFormatter, showSecondAxis=false}) { const chartRef = useRef(); const theme = useTheme(); const { width, height, ref:containerRef } = useResizeDetector(); @@ -100,6 +69,7 @@ export default function StreamingChart({xRange=75, data, options, showSecondAxis ...(data.datasets?.map((datum, index) => ({ label: datum.label, stroke: datum.borderColor, + value: valueFormatter ? (_u, t)=>valueFormatter(t) : undefined, width: options.lineBorderWidth ?? 1, scale: showSecondAxis && (index === 1) ? 'y1' : 'y', points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius * 2 }, @@ -113,59 +83,57 @@ export default function StreamingChart({xRange=75, data, options, showSecondAxis }, ]; + const yAxesValues = (self, values) => { + if(valueFormatter && values) { + return values.map((value) => { + return valueFormatter(value); + }); + } + return values ?? []; + }; + + // ref: https://raw.githubusercontent.com/leeoniya/uPlot/master/demos/axis-autosize.html + const yAxesSize = (self, values, axisIdx, cycleNum) => { + let axis = self.axes[axisIdx]; + + // bail out, force convergence + if (cycleNum > 1) + return axis._size; + + let axisSize = axis.ticks.size + axis.gap + 8; + + // find longest value + let longestVal = (values ?? []).reduce((acc, val) => ( + val.length > acc.length ? val : acc + ), ''); + + if (longestVal != '') { + self.ctx.font = axis.font[0]; + axisSize += self.ctx.measureText(longestVal).width / devicePixelRatio; + } + + return Math.ceil(axisSize); + }; + + axes.push({ + scale: 'y', + grid: { + stroke: theme.otherVars.borderColor, + width: 0.5, + }, + stroke: theme.palette.text.primary, + size: yAxesSize, + values: valueFormatter ? yAxesValues : undefined, + }); + if(showSecondAxis){ - axes.push({ - scale: 'y', - grid: { - stroke: theme.otherVars.borderColor, - width: 0.5, - }, - stroke: theme.palette.text.primary, - size: function(_obj, values) { - let size = 40; - if(values?.length > 0) { - size = values[values.length-1].length*12; - if(size < 40) size = 40; - } - return size; - }, - // y-axis configuration - values: (self, ticks) => { return formatLabel(ticks); } - }); axes.push({ scale: 'y1', side: 1, stroke: theme.palette.text.primary, grid: {show: false}, - size: function(_obj, values) { - let size = 40; - if(values?.length > 0) { - size = values[values.length-1].length*12; - if(size < 40) size = 40; - } - return size; - }, - // y-axis configuration - values: (self, ticks) => { return formatLabel(ticks); } - }); - } else{ - axes.push({ - scale: 'y', - grid: { - stroke: theme.otherVars.borderColor, - width: 0.5, - }, - stroke: theme.palette.text.primary, - size: function(_obj, values) { - let size = 40; - if(values?.length > 0) { - size = values[values.length-1].length*12; - if(size < 40) size = 40; - } - return size; - }, - // y-axis configuration - values: (self, ticks) => { return formatLabel(ticks); } + size: yAxesSize, + values: valueFormatter ? yAxesValues : undefined, }); } @@ -188,6 +156,8 @@ export default function StreamingChart({xRange=75, data, options, showSecondAxis scales: { x: { time: false, + auto: false, + range: [0, xRange-1], } }, axes: axes, @@ -198,16 +168,18 @@ export default function StreamingChart({xRange=75, data, options, showSecondAxis const initialState = [ Array.from(new Array(xRange).keys()), ...(data.datasets?.map((d)=>{ - let ret = [...d.data]; + let ret = new Array(xRange).fill(null); + ret.splice(0, d.data.length, ...d.data); ret.reverse(); return ret; })??{}), ]; - chartRef.current?.setScale('x', {min: data.datasets[0]?.data?.length-xRange, max: data.datasets[0]?.data?.length-1}); return (
- chartRef.current=obj} /> + { + chartRef.current=obj; + }} resetScales={false} />
); } @@ -222,4 +194,5 @@ StreamingChart.propTypes = { data: propTypeData.isRequired, options: PropTypes.object, showSecondAxis: PropTypes.bool, + valueFormatter: PropTypes.func, }; diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index d017b5c62a2..358777eff7d 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -444,9 +444,9 @@ export function downloadBlob(blob, fileName) { document.body.removeChild(link); } -export function toPrettySize(rawSize) { +export function toPrettySize(rawSize, from='B') { try { - let conVal = convert(rawSize).from('B').toBest(); + let conVal = convert(rawSize).from(from).toBest(); conVal.val = Math.round(conVal.val * 100) / 100; return `${conVal.val} ${conVal.unit}`; }