Skip to content

Commit

Permalink
Fix following issues in system stats:
Browse files Browse the repository at this point in the history
1. Graphs rendering in opposite directions on tab change.
2. Y-axis label width should be dynamic.
3. Tooltip values should be formatted.
  • Loading branch information
adityatoshniwal committed Oct 17, 2023
1 parent b4b2a4f commit 344c236
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 129 deletions.
3 changes: 3 additions & 0 deletions web/pgadmin/dashboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
8 changes: 4 additions & 4 deletions web/pgadmin/dashboard/static/js/SystemStats/CPU.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
Expand Down
6 changes: 4 additions & 2 deletions web/pgadmin/dashboard/static/js/SystemStats/Memory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,14 @@ export function MemoryWrapper(props) {
<Grid container spacing={1} className={classes.container}>
<Grid item md={6} sm={12}>
<ChartContainer id='m-graph' title={gettext('Memory')} datasets={props.memoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.memoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
<StreamingChart data={props.memoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={toPrettySize}/>
</ChartContainer>
</Grid>
<Grid item md={6} sm={12}>
<ChartContainer id='sm-graph' title={gettext('Swap memory')} datasets={props.swapMemoryUsageInfo.datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={props.swapMemoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
<StreamingChart data={props.swapMemoryUsageInfo} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={toPrettySize}/>
</ChartContainer>
</Grid>
</Grid>
Expand Down
53 changes: 28 additions & 25 deletions web/pgadmin/dashboard/static/js/SystemStats/Storage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<Grid container spacing={1} className={classes.diskInfoContainer}>
Expand Down Expand Up @@ -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,
}}
/>
</ChartContainer>
Expand Down Expand Up @@ -502,11 +506,7 @@ export function StorageWrapper(props) {
},
},
},
plugins: {
legend: {
display: false,
},
},
...chartJsExtraOptions,
}
}
/>
Expand All @@ -524,8 +524,11 @@ export function StorageWrapper(props) {
<Grid container spacing={1} className={classes.driveContainerBody}>
{Object.keys(props.ioInfo[drive]).map((type, innerKeyIndex) => (
<Grid key={`${type}-${innerKeyIndex}`} item md={4} sm={6}>
<ChartContainer id={`io-graph-${type}`} title={type.endsWith('_bytes_rw') ? gettext('Data transfer (bytes)'): type.endsWith('_total_rw') ? gettext('I/O operations count'): type.endsWith('_time_rw') ? gettext('Time spent in I/O operations (milliseconds)'):''} datasets={transformData(props.ioInfo[drive][type], props.ioRefreshRate).datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={transformData(props.ioInfo[drive][type], props.ioRefreshRate)} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options} />
<ChartContainer id={`io-graph-${type}`} title={type.endsWith('_bytes_rw') ? gettext('Data transfer'): type.endsWith('_total_rw') ? gettext('I/O operations count'): type.endsWith('_time_rw') ? gettext('Time spent in I/O operations'):''} datasets={transformData(props.ioInfo[drive][type], props.ioRefreshRate).datasets} errorMsg={props.errorMsg} isTest={props.isTest}>
<StreamingChart data={transformData(props.ioInfo[drive][type], props.ioRefreshRate)} dataPointSize={DATA_POINT_SIZE} xRange={X_AXIS_LENGTH} options={options}
valueFormatter={(v)=>{
return type.endsWith('_time_rw') ? toPrettySize(v, 'ms') : toPrettySize(v);
}} />
</ChartContainer>
</Grid>
))}
Expand Down Expand Up @@ -556,4 +559,4 @@ StorageWrapper.propTypes = {
lineBorderWidth: PropTypes.number.isRequired,
isDatabase: PropTypes.bool.isRequired,
isTest: PropTypes.bool,
};
};
6 changes: 4 additions & 2 deletions web/pgadmin/dashboard/static/js/SystemStats/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
}
});
Expand Down
161 changes: 67 additions & 94 deletions web/pgadmin/static/js/components/PgChart/StreamingChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -61,21 +32,19 @@ function tooltipPlugin(refreshRate) {

let tooltipHtml=`<div>${(u.data[1].length-1-parseInt(u.legend.values[0]['_'])) * refreshRate + gettext(' seconds ago')}</div>`;
for(let i=1; i<u.series.length; i++) {
let _tooltip = parseFloat(u.legend.values[i]['_'].replace(/,/g,''));
if (_tooltip > 1) _tooltip = parseLabel(_tooltip);
tooltipHtml += `<div class='uplot-tooltip-label'><div style='height:12px; width:12px; background-color:${u.series[i].stroke()}'></div> ${u.series[i].label}: ${_tooltip}</div>`;
tooltipHtml += `<div class='uplot-tooltip-label'><div style='height:12px; width:12px; background-color:${u.series[i].stroke()}'></div> ${u.series[i].label}: ${u.legend.values[i]['_']}</div>`;
}
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 {
Expand All @@ -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();
Expand All @@ -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 },
Expand All @@ -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,
});
}

Expand All @@ -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,
Expand All @@ -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 (
<div ref={containerRef} style={{width: '100%', height: '100%'}}>
<UplotReact target={containerRef.current} options={defaultOptions} data={initialState} onCreate={(obj)=>chartRef.current=obj} />
<UplotReact target={containerRef.current} options={defaultOptions} data={initialState} onCreate={(obj)=>{
chartRef.current=obj;
}} resetScales={false} />
</div>
);
}
Expand All @@ -222,4 +194,5 @@ StreamingChart.propTypes = {
data: propTypeData.isRequired,
options: PropTypes.object,
showSecondAxis: PropTypes.bool,
valueFormatter: PropTypes.func,
};
Loading

0 comments on commit 344c236

Please sign in to comment.