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.
pull/6866/head
Aditya Toshniwal 2023-10-17 15:01:52 +05:30
parent b4b2a4ff67
commit 344c236d72
7 changed files with 112 additions and 129 deletions

View File

@ -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'])

View File

@ -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});
}

View File

@ -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>

View File

@ -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}>
@ -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>
@ -502,11 +506,7 @@ export function StorageWrapper(props) {
},
},
},
plugins: {
legend: {
display: false,
},
},
...chartJsExtraOptions,
}
}
/>
@ -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>
))}
@ -556,4 +559,4 @@ StorageWrapper.propTypes = {
lineBorderWidth: PropTypes.number.isRequired,
isDatabase: PropTypes.bool.isRequired,
isTest: PropTypes.bool,
};
};

View File

@ -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,
];
}
});

View File

@ -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=`<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 {
@ -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 (
<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>
);
}
@ -222,4 +194,5 @@ StreamingChart.propTypes = {
data: propTypeData.isRequired,
options: PropTypes.object,
showSecondAxis: PropTypes.bool,
valueFormatter: PropTypes.func,
};

View File

@ -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}`;
}