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
parent
b4b2a4ff67
commit
344c236d72
|
@ -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'])
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue