mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
build: Chart.js 4.5.1
This commit is contained in:
parent
53d900fd57
commit
f5ec757d1a
9 changed files with 164 additions and 404 deletions
|
|
@ -99,7 +99,7 @@
|
|||
{% bs_icon 'arrow-right' %}
|
||||
{% trans "CPU Utilization" %}
|
||||
</h5>
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
<div class="chart"><canvas id="cpuChart" role="img" aria-label="{% trans "CPU Utilization" %}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
<h5 class="card-title ">
|
||||
{% bs_icon 'arrow-right'%} {% trans "RAM Utilization" %}
|
||||
</h5>
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
<div class="chart"><canvas id="memChart" role="img" aria-label="{% trans "RAM Utilization" %}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -116,114 +116,22 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/Chart.bundle.min.js" %}"></script>
|
||||
<script src="{% static "js/chart.umd.min.js" %}"></script>
|
||||
<script src="{% static "js/charts.js" %}"></script>
|
||||
<script src="{% static "js/poll.js" %}"></script>
|
||||
<script>
|
||||
|
||||
{% trans "average over" as average_over %}
|
||||
// the seconds each CPU point averages
|
||||
var cpuWindows = [];
|
||||
var cpu_ctx = document.getElementById("cpuChart").getContext("2d");
|
||||
var cpuChart = new Chart(cpu_ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets : [{
|
||||
label: 'Usage',
|
||||
backgroundColor: "rgba(241,72,70,0.5)",
|
||||
pointRadius: 2,
|
||||
}]
|
||||
var cpuChart = lineChart(document.getElementById("cpuChart"), "%", ["Usage"], {
|
||||
max: 100,
|
||||
tooltip: function (context, text) {
|
||||
var span = cpuWindows[context.dataIndex];
|
||||
return span ? text + ' ({{ average_over|escapejs }} ' + span + ' s)' : text;
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
stepSize: 10,
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
max: 100,
|
||||
min: 0,
|
||||
stepSize: 20,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' %';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += tooltipItem.yLabel + ' %';
|
||||
var span = cpuWindows[tooltipItem.index];
|
||||
return span ? label + ' ({{ average_over|escapejs }} ' + span + ' s)' : label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var mem_ctx = document.getElementById("memChart").getContext("2d");
|
||||
var memChart = new Chart(mem_ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
pointRadius: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks:{
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 100,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' MB';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
return label += tooltipItem.yLabel + ' MB';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MB", ["Usage"]);
|
||||
if (Boolean({{ status }}) === true) {
|
||||
// the CPU sample of the previous answer: usage is taken since then
|
||||
var cpuSample = null;
|
||||
|
|
@ -231,32 +139,18 @@
|
|||
return $.getJSON('{% url 'compute_graph' compute_id %}', cpuSample ? {cpu_sample: cpuSample} : {});
|
||||
}, function graph_usage(data) {
|
||||
cpuSample = data.cpu_sample || null;
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
memChart.data.labels.push(data.timeline);
|
||||
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
cpuWindows.push(data.window);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
if (cpuWindows.length > CHART_POINTS) {
|
||||
cpuWindows.shift();
|
||||
}
|
||||
addPoint(cpuChart, data.timeline, [data.cpudata]);
|
||||
// a host that could not be read gives no memory value: a gap, same axis
|
||||
if (data.memdata.total) {
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1048576);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1048576 * 5));
|
||||
memChart.data.datasets[0].data.push(parseInt(data.memdata.usage / 1048576));
|
||||
memChart.options.scales.y.max = Math.round(data.memdata.total / 1048576);
|
||||
addPoint(memChart, data.timeline, [Math.round(data.memdata.usage / 1048576)]);
|
||||
} else {
|
||||
memChart.data.datasets[0].data.push(null);
|
||||
addPoint(memChart, data.timeline, [null]);
|
||||
}
|
||||
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
}
|
||||
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import unittest
|
|||
from unittest import TestCase as UnitTestCase
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import signing
|
||||
from django.test import TestCase
|
||||
|
|
@ -20,6 +21,9 @@ from computes.models import Compute
|
|||
from computes.views import CPU_SAMPLE_MAX_AGE, CPU_SAMPLE_SIGNER
|
||||
from vrtManager.hostdetails import cpu_percent, wvmHostDetails
|
||||
|
||||
# the page script runs after the shared chart helpers
|
||||
CHARTS_JS = settings.BASE_DIR / "static" / "js" / "charts.js"
|
||||
|
||||
|
||||
def host(*readings):
|
||||
"""A wvmHostDetails whose getCPUStats returns readings, (idle, busy) each."""
|
||||
|
|
@ -133,19 +137,22 @@ class GraphScriptTestCase(TestCase):
|
|||
const charts = [], requests = [];
|
||||
let poll;
|
||||
const page = {
|
||||
document: {getElementById: () => ({getContext: () => ({})})},
|
||||
document: {body: {}, getElementById: () => ({})},
|
||||
// Chart.js adds the lists a config leaves out
|
||||
Chart: function (ctx, config) {
|
||||
getComputedStyle: () => ({getPropertyValue: () => ""}),
|
||||
Chart: Object.assign(function (canvas, config) {
|
||||
this.data = Object.assign({labels: []}, config.data);
|
||||
this.data.datasets.forEach((set) => { set.data = set.data || []; });
|
||||
this.options = config.options;
|
||||
this.update = () => {};
|
||||
charts.push(this);
|
||||
},
|
||||
}, {defaults: {}}),
|
||||
$: {getJSON: (url, params) => { requests.push(params); return {}; }},
|
||||
pollWhileVisible: (interval, request, onData) => { poll = {request, onData}; },
|
||||
};
|
||||
vm.runInNewContext(SCRIPT, vm.createContext(page));
|
||||
const context = vm.createContext(page);
|
||||
vm.runInContext(CHARTS, context);
|
||||
vm.runInContext(SCRIPT, context);
|
||||
const answer = (cpu, sample) => ({cpudata: cpu, window: 5.1, cpu_sample: sample, timeline: "t",
|
||||
memdata: {total: 1048576, usage: 0}});
|
||||
poll.request();
|
||||
|
|
@ -153,10 +160,10 @@ class GraphScriptTestCase(TestCase):
|
|||
poll.request();
|
||||
poll.onData({cpudata: null, memdata: {total: null, usage: null}, timeline: "t"});
|
||||
poll.request();
|
||||
const label = charts[0].options.tooltips.callbacks.label({datasetIndex: 0, index: 0, yLabel: 3}, charts[0].data);
|
||||
const label = charts[0].options.plugins.tooltip.callbacks.label({dataset: {label: "Usage"}, formattedValue: "3", dataIndex: 0});
|
||||
console.log(JSON.stringify({requests, label, cpu: charts[0].data.datasets[0].data,
|
||||
mem: charts[1].data.datasets[0].data, memMax: charts[1].options.scales.yAxes[0].ticks.max}));
|
||||
""".replace("SCRIPT", json.dumps(script))
|
||||
mem: charts[1].data.datasets[0].data, memMax: charts[1].options.scales.y.max}));
|
||||
""".replace("SCRIPT", json.dumps(script)).replace("CHARTS", json.dumps(CHARTS_JS.read_text()))
|
||||
out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(out.returncode, 0, out.stderr)
|
||||
result = json.loads(out.stdout)
|
||||
|
|
|
|||
|
|
@ -380,7 +380,8 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/Chart.bundle.min.js' %}"></script>
|
||||
<script src="{% static 'js/chart.umd.min.js' %}"></script>
|
||||
<script src="{% static 'js/charts.js' %}"></script>
|
||||
<script src="{% static 'js/poll.js' %}"></script>
|
||||
<script>
|
||||
{% trans "average over" as average_over %}
|
||||
|
|
@ -399,222 +400,21 @@
|
|||
stats_poll.start();
|
||||
return;
|
||||
}
|
||||
var cpu_ctx = $("#cpuChart").get(0).getContext("2d");
|
||||
var cpuChart = new Chart(cpu_ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets : [{
|
||||
backgroundColor: "rgba(44,127,184,0.5)",
|
||||
label: "Usage"
|
||||
}]
|
||||
var cpuChart = lineChart(document.getElementById("cpuChart"), "%", ["Usage"], {
|
||||
max: 100,
|
||||
tooltip: function (context, text) {
|
||||
var span = cpuWindows[context.dataIndex];
|
||||
return span ? text + ' ({{ average_over|escapejs }} ' + span + ' s)' : text;
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
stepSize: 10,
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
suggestedMax: 100,
|
||||
suggestedMin: 0,
|
||||
stepSize: 20,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' %';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += tooltipItem.yLabel + ' %';
|
||||
var span = cpuWindows[tooltipItem.index];
|
||||
return span ? label + ' ({{ average_over|escapejs }} ' + span + ' s)' : label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var mem_ctx = $("#memChart").get(0).getContext("2d");
|
||||
var memChart = new Chart(mem_ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets : [{
|
||||
label: "Usage"
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
stepSize: 10,
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
suggestedMax: 100,
|
||||
suggestedMin: 0,
|
||||
stepSize: 20,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' MB';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += '(RSS): ';
|
||||
}
|
||||
return label += tooltipItem.yLabel + ' MB';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MB", ["Usage (RSS)"]);
|
||||
var diskChart = {};
|
||||
{% for disk in instance.disks %}
|
||||
var disk_ctx_{{ disk.dev }} = $("#blk{{ disk.dev }}Chart").get(0).getContext("2d");
|
||||
diskChart['{{ disk.dev }}'] = new Chart(disk_ctx_{{ disk.dev }}, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets : [{
|
||||
backgroundColor: "rgba(127,205,187,0.5)",
|
||||
label: "Read"
|
||||
},
|
||||
{
|
||||
backgroundColor: "rgba(44,127,184,0.5)",
|
||||
label: "Write"
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
stepSize: 10,
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
suggestedmax: 100,
|
||||
suggestedMin: 0,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' MB/s';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
return label += tooltipItem.yLabel + ' MB/s';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
diskChart['{{ disk.dev|escapejs }}'] = lineChart(document.getElementById("blk{{ disk.dev|escapejs }}Chart"), "MB/s", ["Read", "Write"]);
|
||||
{% endfor %}
|
||||
|
||||
var netChart = {};
|
||||
{% for net in instance.networks %}
|
||||
var net_ctx_{{ forloop.counter0 }} = $("#netEth{{ forloop.counter0 }}Chart").get(0).getContext("2d");
|
||||
netChart[{{ forloop.counter0 }}] = new Chart(net_ctx_{{ forloop.counter0 }}, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets : [
|
||||
{
|
||||
backgroundColor: "rgba(127,205,187,0.5)",
|
||||
label: "Inbound"
|
||||
},
|
||||
{
|
||||
backgroundColor: "rgba(44,127,184,0.5)",
|
||||
label: "Outbound"
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
stepSize: 10,
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
suggestedMax: 100,
|
||||
suggestedMin: 0,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' Mbps';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
return label += tooltipItem.yLabel + ' Mbps';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
netChart[{{ forloop.counter0 }}] = lineChart(document.getElementById("netEth{{ forloop.counter0 }}Chart"), "Mbit/s", ["Inbound", "Outbound"]);
|
||||
{% endfor %}
|
||||
|
||||
// the first point at once, then every 5 s like the host graph
|
||||
|
|
@ -625,40 +425,23 @@
|
|||
});
|
||||
}, function graph_usage(data) {
|
||||
statsSample = data.sample || null;
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
cpuWindows.push(data.window);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
if (cpuWindows.length > CHART_POINTS) {
|
||||
cpuWindows.shift();
|
||||
}
|
||||
cpuChart.update();
|
||||
|
||||
memChart.data.labels.push(data.timeline);
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1024);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1024 * 5));
|
||||
memChart.data.datasets[0].data.push(data.memdata.used / 1024);
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
addPoint(cpuChart, data.timeline, [data.cpudata]);
|
||||
// a VM whose memory could not be read gives total 0: a gap, same axis
|
||||
if (data.memdata.total) {
|
||||
memChart.options.scales.y.max = Math.round(data.memdata.total / 1024);
|
||||
addPoint(memChart, data.timeline, [Math.round(data.memdata.used / 1024)]);
|
||||
} else {
|
||||
addPoint(memChart, data.timeline, [null]);
|
||||
}
|
||||
memChart.update();
|
||||
|
||||
// a disk or NIC added after the page loaded has no chart until a reload
|
||||
function push(chart, values) {
|
||||
if (!chart) {
|
||||
return;
|
||||
if (chart) {
|
||||
addPoint(chart, data.timeline, values);
|
||||
}
|
||||
chart.data.labels.push(data.timeline);
|
||||
chart.data.datasets[0].data.push(values[0]);
|
||||
chart.data.datasets[1].data.push(values[1]);
|
||||
if (chart.data.datasets[0].data.length > 10){
|
||||
chart.data.labels.shift();
|
||||
chart.data.datasets[0].data.shift();
|
||||
chart.data.datasets[1].data.shift();
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
data.blkdata.forEach(function (disk) { push(diskChart[disk.dev], disk.data); });
|
||||
data.netdata.forEach(function (nic) { push(netChart[nic.dev], nic.data); });
|
||||
|
|
|
|||
|
|
@ -24,11 +24,7 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart"><canvas id="cpuChart" role="img" aria-label="{% trans "CPU Usage" %}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 card border-danger">
|
||||
|
|
@ -38,11 +34,7 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart"><canvas id="memChart" role="img" aria-label="{% trans "Memory Usage" %}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
{% for net in instance.networks %}
|
||||
|
|
@ -53,11 +45,7 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="netEth{{ forloop.counter0 }}Chart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart"><canvas id="netEth{{ forloop.counter0 }}Chart" role="img" aria-label="{% trans "Bandwidth Device" %}: eth{{ forloop.counter0 }}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
@ -69,11 +57,7 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="blk{{ disk.dev }}Chart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart"><canvas id="blk{{ disk.dev }}Chart" role="img" aria-label="{% trans "Disk I/O device" %}: {{ disk.dev }}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from unittest.mock import patch
|
|||
|
||||
import libvirt
|
||||
from computes.models import Compute
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import signing
|
||||
from django.test import TestCase
|
||||
|
|
@ -18,6 +19,9 @@ from lxml import html
|
|||
from instances.models import Instance
|
||||
from vrtManager.instance import USAGE_MAX_WINDOW
|
||||
|
||||
# the page script runs after the shared chart helpers
|
||||
CHARTS_JS = settings.BASE_DIR / "static" / "js" / "charts.js"
|
||||
|
||||
SAMPLE = {"key": [7, 4, ["vda"], [["52:54:00:00:00:01", "vnet0"]]], "counters": [1, [[2, 3]], [[4, 5]], 100.0]}
|
||||
|
||||
|
||||
|
|
@ -153,42 +157,51 @@ class StatsScriptTestCase(TestDriverVM, TestCase):
|
|||
};
|
||||
const page = {
|
||||
$,
|
||||
document: {querySelector: () => ({addEventListener: (name, handler) => { handlers[name] = handler; }})},
|
||||
document: {body: {}, getElementById: () => ({}),
|
||||
querySelector: () => ({addEventListener: (name, handler) => { handlers[name] = handler; }})},
|
||||
// Chart.js adds the lists a config leaves out
|
||||
Chart: function (ctx, config) {
|
||||
getComputedStyle: () => ({getPropertyValue: () => ""}),
|
||||
Chart: Object.assign(function (canvas, config) {
|
||||
this.data = Object.assign({labels: []}, config.data);
|
||||
this.data.datasets.forEach((set) => { set.data = set.data || []; });
|
||||
this.options = config.options;
|
||||
this.update = () => {};
|
||||
charts.push(this);
|
||||
},
|
||||
}, {defaults: {}}),
|
||||
pollWhileVisible: (interval, request, onData, first) => { poll = {interval, first, request, onData}; return {}; },
|
||||
};
|
||||
vm.runInNewContext(SCRIPT, vm.createContext(page));
|
||||
const context = vm.createContext(page);
|
||||
vm.runInContext(CHARTS, context);
|
||||
vm.runInContext(SCRIPT, context);
|
||||
handlers["shown.bs.tab"]();
|
||||
const answer = (cpu, sample) => ({cpudata: cpu, window: 9.9, sample: sample, timeline: "t",
|
||||
memdata: {used: 0, total: 1024},
|
||||
memdata: {used: 1024, total: 1024},
|
||||
// vdb was attached after the page loaded
|
||||
blkdata: [{dev: "vdb", data: [9, 9]}, {dev: "vda", data: [1.5, null]}],
|
||||
netdata: [{dev: 0, data: [2, 3]}]});
|
||||
poll.request();
|
||||
poll.onData(answer(3, "token-1"));
|
||||
poll.request();
|
||||
// a VM whose memory could not be read
|
||||
poll.onData(Object.assign(answer(4, "token-2"), {memdata: {used: 0, total: 0}}));
|
||||
fail = true;
|
||||
poll.request();
|
||||
fail = false;
|
||||
poll.request();
|
||||
const label = charts[0].options.tooltips.callbacks.label({datasetIndex: 0, index: 0, yLabel: 3}, charts[0].data);
|
||||
const label = charts[0].options.plugins.tooltip.callbacks.label({dataset: {label: "Usage"}, formattedValue: "3", dataIndex: 0});
|
||||
console.log(JSON.stringify({requests, label, every: [poll.interval, poll.first], cpu: charts[0].data.datasets[0].data,
|
||||
disk: charts[2].data.datasets[1].data, net: charts[3].data.datasets[0].data}));
|
||||
""".replace("SCRIPT", json.dumps(script))
|
||||
disk: charts[2].data.datasets[1].data, net: charts[3].data.datasets[0].data,
|
||||
mem: charts[1].data.datasets[0].data, memMax: charts[1].options.scales.y.max}));
|
||||
""".replace("SCRIPT", json.dumps(script)).replace("CHARTS", json.dumps(CHARTS_JS.read_text()))
|
||||
out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(out.returncode, 0, out.stderr)
|
||||
result = json.loads(out.stdout)
|
||||
# the first poll at once, then every 5 s
|
||||
self.assertEqual(result["every"], [5000, 0])
|
||||
# the sample goes with the next poll; a failed poll drops it
|
||||
self.assertEqual(result["requests"], [{}, {"sample": "token-1"}, {"sample": "token-1"}, {}])
|
||||
self.assertEqual(result["requests"], [{}, {"sample": "token-1"}, {"sample": "token-2"}, {}])
|
||||
# a disk without a chart is skipped, the other charts still update
|
||||
self.assertEqual((result["cpu"], result["disk"], result["net"]), ([3], [None], [2]))
|
||||
self.assertEqual((result["cpu"], result["disk"], result["net"]), ([3, 4], [None, None], [2, 2]))
|
||||
# memory that could not be read leaves a gap and keeps the axis
|
||||
self.assertEqual((result["mem"], result["memMax"]), ([1, None], 1))
|
||||
self.assertTrue(result["label"].endswith(": 3 % (average over 9.9 s)"), result["label"])
|
||||
|
|
|
|||
|
|
@ -93,8 +93,10 @@ p {
|
|||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.flot-chart {
|
||||
max-height: 250px;
|
||||
/* a live chart (static/js/charts.js) fills the width of its card */
|
||||
.chart {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.netIN {
|
||||
|
|
|
|||
7
static/js/Chart.bundle.min.js
vendored
7
static/js/Chart.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
14
static/js/chart.umd.min.js
vendored
Normal file
14
static/js/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
70
static/js/charts.js
Normal file
70
static/js/charts.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Live line charts of the host overview and the VM page (Chart.js 4): the
|
||||
// last CHART_POINTS readings, a gap where a reading is missing, and the
|
||||
// colours of the selected Bootstrap theme.
|
||||
var CHART_POINTS = 10;
|
||||
|
||||
(function () {
|
||||
var style = getComputedStyle(document.body);
|
||||
Chart.defaults.color = style.getPropertyValue("--bs-body-color").trim();
|
||||
Chart.defaults.borderColor = style.getPropertyValue("--bs-border-color").trim();
|
||||
})();
|
||||
|
||||
// labels: one per line. options: max (the y axis top), tooltip(context, text)
|
||||
// to add to a point's tooltip text.
|
||||
function lineChart(canvas, unit, labels, options) {
|
||||
var style = getComputedStyle(document.body);
|
||||
var colours = ["--bs-primary-rgb", "--bs-info-rgb"].map(function (name) {
|
||||
return style.getPropertyValue(name).trim();
|
||||
});
|
||||
options = options || {};
|
||||
return new Chart(canvas, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: labels.map(function (label, i) {
|
||||
return {
|
||||
label: label,
|
||||
data: [],
|
||||
borderColor: "rgb(" + colours[i] + ")",
|
||||
backgroundColor: "rgba(" + colours[i] + ", 0.3)",
|
||||
fill: true,
|
||||
pointRadius: 2,
|
||||
};
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {ticks: {maxTicksLimit: CHART_POINTS, maxRotation: 0}},
|
||||
y: {min: 0, max: options.max, ticks: {callback: function (value) { return value + " " + unit; }}},
|
||||
},
|
||||
plugins: {
|
||||
legend: {display: labels.length > 1},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
var text = context.dataset.label + ": " + context.formattedValue + " " + unit;
|
||||
return options.tooltip ? options.tooltip(context, text) : text;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// values: one per line, null for a reading that is missing
|
||||
function addPoint(chart, label, values) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets.forEach(function (set, i) {
|
||||
set.data.push(values[i] == null ? null : values[i]);
|
||||
});
|
||||
if (chart.data.labels.length > CHART_POINTS) {
|
||||
chart.data.labels.shift();
|
||||
chart.data.datasets.forEach(function (set) {
|
||||
set.data.shift();
|
||||
});
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue